Compare commits
442 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe0378e9b3 | |||
| 96a1d7c645 | |||
| 79ee8b46f4 | |||
| 55a7a8b8c9 | |||
| d1d66b9c5f | |||
| 6dacbb451f | |||
| ead9cefadb | |||
| 185a2fc55e | |||
| fb8fac6c60 | |||
| b6f288a1ce | |||
| aa9bec96b1 | |||
| b16ff9f859 | |||
| 348c5c4838 | |||
| 8dcc6a0280 | |||
| 5e029f7600 | |||
| d8d33e8b8b | |||
| b37f7d49d8 | |||
| d67d5dd963 | |||
| 273e0d42b7 | |||
| ca497a82ab | |||
| b7226316c7 | |||
| 84f41954ae | |||
| 54da339b2c | |||
| ac37fcf6f3 | |||
| 893c974b08 | |||
| 30342efa37 | |||
| 6165c246d4 | |||
| 72befeef24 | |||
| af2a92f22b | |||
| ad2f826a82 | |||
| e095a1572f | |||
| c3dd6e1926 | |||
| 67ecd2cb82 | |||
| 57d751c377 | |||
| 50075106b6 | |||
| 2a1f8f6fda | |||
| 1c817913ee | |||
| de0a48bd6f | |||
| a11fc9f067 | |||
| e12a204bcc | |||
| fe014a8e6c | |||
| aa8ea84d11 | |||
| 3175e56ad0 | |||
| 1a201d2433 | |||
| 750c94efbb | |||
| bd144a64f6 | |||
| 5ed4386bbf | |||
| 9d3ec7b39f | |||
| e68a23bdc1 | |||
| 6cf493bea7 | |||
| 3d5633a0a0 | |||
| c4a44f6f0b | |||
| 3e29695c1f | |||
| 46a9f29bae | |||
| 67239957c9 | |||
| d4e62101ab | |||
| 4fdf11b2e6 | |||
| cd0f22ef72 | |||
| e5c0b31107 | |||
| 5cdbd2ed7a | |||
| b44e2e71aa | |||
| 73afc6311d | |||
| 6127d757a7 | |||
| fb86dcfb17 | |||
| bccf06c748 | |||
| 862e223cec | |||
| e1e2ff52fe | |||
| d03edf12e4 | |||
| ec1dfc521c | |||
| 5190f7f33a | |||
| 873a5aa8e7 | |||
| 672d3a6c6c | |||
| a749fb2130 | |||
| 25d1bc2c09 | |||
| cc63c1b584 | |||
| 145c622aba | |||
| e2516c01b4 | |||
| a3cb18d0f0 | |||
| eca9f9c1a1 | |||
| aee845682f | |||
| e3dbe2f2ba | |||
| 193888a2b4 | |||
| 9fe8bfb2bc | |||
| fc25973371 | |||
| f9acd605dc | |||
| 290b1973a9 | |||
| d7d42ff4fe | |||
| ce9e50f4ee | |||
| ecd1fff9b0 | |||
| b0f12bd5e8 | |||
| 5d61ec11e3 | |||
| c792c0a6c9 | |||
| d9d2540162 | |||
| f5d08fc49c | |||
| b24759af1c | |||
| 4d1692726b | |||
| 1581ed52ba | |||
| de1a5a75cc | |||
| 169ef21de7 | |||
| d0fa6927f8 | |||
| 63e8a4ac74 | |||
| 459230d3f9 | |||
| 070e1aec7e | |||
| 3ac68f1966 | |||
| 5ccde4dffc | |||
| dc847001a5 | |||
| 639833aaf5 | |||
| 8f2a80804c | |||
| 78842970cf | |||
| 8e7d4cda07 | |||
| 5b3ad0023b | |||
| 6a1279fb90 | |||
| 66910a7602 | |||
| d9bce2defd | |||
| 352991bdf4 | |||
| 4383d7b603 | |||
| 89ae56820a | |||
| 17489cac1a | |||
| c1a5318d8e | |||
| b0b690cf23 | |||
| 86e83186b5 | |||
| 36d610a388 | |||
| 50b70eeb68 | |||
| cc0f59742f | |||
| 09dd7f14de | |||
| b419699ab8 | |||
| 08825fa611 | |||
| 02f0c1e46d | |||
| e44f62a95c | |||
| dbfb3eb923 | |||
| e43323221b | |||
| da06349723 | |||
| cff2187a4c | |||
| a078d3c872 | |||
| da5bb4db96 | |||
| 1b19939742 | |||
| 930e1d8830 | |||
| fa68fe6ff3 | |||
| 21a5a6202d | |||
| db60337598 | |||
| c5be64fec4 | |||
| 659e846006 | |||
| d8f56352da | |||
| d1a3d020aa | |||
| 8807b7dd46 | |||
| cd155a1f25 | |||
| d8887f3488 | |||
| 1c841d4fee | |||
| da199846d2 | |||
| bd04d7d475 | |||
| 5f93aa0ecf | |||
| 05796bed57 | |||
| 8a131dffb6 | |||
| 79efcb545d | |||
| 88dcba3482 | |||
| 754609ab69 | |||
| d6ab71f450 | |||
| 55edbcd02f | |||
| 90dde9beab | |||
| 5fc1329b2f | |||
| 9c8085a0aa | |||
| 507ea757a5 | |||
| 7e065aaacd | |||
| 0312bbc535 | |||
| a056f1deec | |||
| fdaefe6997 | |||
| 88279439af | |||
| 2d6a49215c | |||
| a7e14a3065 | |||
| a660cc0d01 | |||
| 788d66f409 | |||
| 96988a37f5 | |||
| b368d21568 | |||
| c88b763e80 | |||
| ec3c89e57c | |||
| 5dcab2d361 | |||
| 5f7e98be20 | |||
| d52af3f58f | |||
| 063c838c92 | |||
| 9632bf2287 | |||
| dede1585ee | |||
| 5be7b9af3e | |||
| 5183399f50 | |||
| a780b7c6b5 | |||
| 0ae778c881 | |||
| 1f8b679b18 | |||
| ee5df76579 | |||
| b431720dac | |||
| 42ce68894a | |||
| c063fc0238 | |||
| 0a9ac63a05 | |||
| 6dccdd657f | |||
| 34a434616a | |||
| bc9b91e501 | |||
| edbd3da33a | |||
| 32e8f9beca | |||
| 84ceeaa870 | |||
| cdeaba2acf | |||
| c0b82bd807 | |||
| 88e35b6f80 | |||
| 6e17cc45ea | |||
| cb9d0fd3bc | |||
| 3adf9ce04e | |||
| c2e95997d4 | |||
| 808faa6371 | |||
| 6f511ac29b | |||
| 3dc93e390a | |||
| e2d034e488 | |||
| 86205540d8 | |||
| 702c3538a4 | |||
| 069a7c1e99 | |||
| 2e7649beda | |||
| 8281a0fa1c | |||
| 3491d7d2f1 | |||
| e664a46ff3 | |||
| 0809f649d3 | |||
| 02a002d264 | |||
| 3bfc822578 | |||
| 02c291d13b | |||
| b25bcf1a66 | |||
| fe204e6f48 | |||
| 2b6ca38728 | |||
| c106ed32ea | |||
| 2626d88a21 | |||
| 3a0ffbc772 | |||
| bd9bf9b646 | |||
| 7b6f50772c | |||
| 555552340d | |||
| 6e2c32eb9a | |||
| 22b0a38df5 | |||
| cb58e63fc5 | |||
| 8626598753 | |||
| 36231345f1 | |||
| e8f001d451 | |||
| 13e03a6911 | |||
| fde82f448f | |||
| 79b3265ef1 | |||
| 389db5f598 | |||
| fe88b5068d | |||
| 6746c385bd | |||
| f50e40008f | |||
| 061f8d12e0 | |||
| 38554fc2a7 | |||
| cc7de8df75 | |||
| 30f604517a | |||
| 080f35fe65 | |||
| 78f86ea502 | |||
| 5b8287617d | |||
| 5799806414 | |||
| 76a568fc97 | |||
| 14265d9a18 | |||
| 17235eb089 | |||
| 7f118519eb | |||
| 250e47e2eb | |||
| f413fb8e56 | |||
| f0e43dcdb1 | |||
| abf85b2508 | |||
| 813771e6b7 | |||
| d3f83a6592 | |||
| 7958f1f694 | |||
| 4a02c6dab1 | |||
| 165d343d06 | |||
| 60fd7d686d | |||
| c701de939f | |||
| 05b87d2d5b | |||
| 78e4a385f7 | |||
| 822abab17e | |||
| f1d16ea003 | |||
| fb7eaf67d1 | |||
| e53721ef69 | |||
| 2f67a62b5b | |||
| 79fe864d9a | |||
| 6f7de49aa8 | |||
| 9ee11161bf | |||
| 90f6bdd6e4 | |||
| e49f82b9e1 | |||
| ddf5a1940f | |||
| 00cf2a1fa2 | |||
| 9cc72ff1a9 | |||
| 3384943cd3 | |||
| 2f14dae83f | |||
| f03ebc20aa | |||
| 4b4918f2a7 | |||
| 0425f65e63 | |||
| 452128f0da | |||
| f5fe3ce34e | |||
| d0084ce5f2 | |||
| 2eaa0a1dd7 | |||
| 8085754507 | |||
| c46382ba29 | |||
| b38c6929be | |||
| 42d1c36a5c | |||
| 51a4b65101 | |||
| 30fb45c494 | |||
| 9cdd39e0d7 | |||
| 45a8945746 | |||
| 697282d6ad | |||
| 78a76ad50e | |||
| 5ecfe13be9 | |||
| 0bc1c6d57a | |||
| f57175cbad | |||
| 81a11a3c30 | |||
| 04cca097ae | |||
| 48897b5fa1 | |||
| ecae342434 | |||
| f2383151cb | |||
| b4565af256 | |||
| c85e775162 | |||
| 3491df6edb | |||
| 0e6ec57996 | |||
| f37b158982 | |||
| da54abaf2e | |||
| 092c761cec | |||
| 5edafd6284 | |||
| d64f105b44 | |||
| 2d5eda5141 | |||
| be15d5f2d9 | |||
| 5918a5a322 | |||
| f8af296e6f | |||
| 432e18990b | |||
| 2e9403b047 | |||
| 3ea6a2c7c3 | |||
| 20bf0e00e8 | |||
| dd53c457d7 | |||
| ac599d6528 | |||
| ca4597e9d7 | |||
| eeea9ac946 | |||
| 0a28628c02 | |||
| bcc4754dc1 | |||
| 66d9a73362 | |||
| 5712e37437 | |||
| b1ed75078e | |||
| 47d7486bbe | |||
| d227af1edd | |||
| 4e18010731 | |||
| db3242e4bb | |||
| 7169212683 | |||
| 2a6a1d439c | |||
| 8984aef899 | |||
| b103ac70bf | |||
| 37c20fa64b | |||
| ab0d0a28a8 | |||
| 0de3f1ca9a | |||
| 95d66ebc6b | |||
| 393e873d25 | |||
| 956491f853 | |||
| 302660e362 | |||
| 5e6cd21f8b | |||
| 9e1cd37bbc | |||
| 8d4282cd36 | |||
| 1e0738f63f | |||
| f355d33b5f | |||
| 968e08a9ba | |||
| 2cc22de696 | |||
| 12c9b9b3c9 | |||
| a11e61677c | |||
| 01f446e908 | |||
| f4a4cfd2cc | |||
| eaa2183d77 | |||
| 31d2b258c1 | |||
| 4b3a381f39 | |||
| 56473d4cce | |||
| efa7ea592c | |||
| afd325a884 | |||
| a3f6054f97 | |||
| da937bf214 | |||
| 42b63eb818 | |||
| 0d6db333d6 | |||
| 3999465c85 | |||
| 1cc4049e82 | |||
| 4107701062 | |||
| a799cdad3e | |||
| a118ad90ed | |||
| 0f23fb949d | |||
| f1992eeea5 | |||
| 84d68007cb | |||
| bf63cb9045 | |||
| ce0041832c | |||
| 97d5f525f4 | |||
| 2ea29ce0ef | |||
| 068076f775 | |||
| 34c8b24211 | |||
| e3cc625315 | |||
| f67ea78cce | |||
| 6255112926 | |||
| c906239220 | |||
| b4682e6707 | |||
| 04050c4173 | |||
| 7e6ede6379 | |||
| 63e80384ea | |||
| 7ef9833dbb | |||
| c1ee9bf881 | |||
| c000ef194c | |||
| 479ac9afa7 | |||
| 716892b95d | |||
| d7a6485dfe | |||
| fd224ee590 | |||
| 3922691fb9 | |||
| c566c8efc7 | |||
| 06b585ce8a | |||
| e61af8bc62 | |||
| b6825f98c0 | |||
| 86ada2fa5d | |||
| b515a5a9ec | |||
| 6d5bdff394 | |||
| 0ca8844398 | |||
| 10ef4f7f39 | |||
| cff3b37a61 | |||
| d26a3b37a6 | |||
| 82dd963e08 | |||
| 830c458fe7 | |||
| 38f29f7d0c | |||
| 7e59b83053 | |||
| a55a0d370d | |||
| ca902b6be4 | |||
| 844a8db6c6 | |||
| 3dd1e4d58c | |||
| 62c78696cd | |||
| 9060b5c2f5 | |||
| 3afdd82e42 | |||
| 5aa95b667c | |||
| 054451fd19 | |||
| 49e656839f | |||
| c05e9f856d | |||
| 7e92302c4f | |||
| 94f0d478de | |||
| 2eb4e2a0b8 | |||
| ea9095c562 | |||
| 1c946ef003 | |||
| b45143da9b | |||
| ed56b6a905 | |||
| 6fce89e60b | |||
| 4489005cb2 | |||
| 67b20f2c8c | |||
| 6102552d61 | |||
| d7673274d2 | |||
| f01990aad2 | |||
| db1e965b65 | |||
| 2ae8aaa106 | |||
| c80448c4d1 | |||
| c75942c79d | |||
| a91b710961 |
@@ -19,3 +19,7 @@ Andy Smith <github@anarkystic.com>
|
||||
<victor.vieux@dotcloud.com> <dev@vvieux.com>
|
||||
<dominik@honnef.co> <dominikh@fork-bomb.org>
|
||||
Thatcher Peskens <thatcher@dotcloud.com>
|
||||
<ehanchrow@ine.com> <eric.hanchrow@gmail.com>
|
||||
Walter Stanish <walter@pratyeka.org>
|
||||
<daniel@gasienica.ch> <dgasienica@zynga.com>
|
||||
Roberto Hashioka <roberto_hashioka@hotmail.com>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
Al Tobey <al@ooyala.com>
|
||||
Alexey Shamrin <shamrin@gmail.com>
|
||||
Andrea Luzzardi <aluzzardi@gmail.com>
|
||||
Andreas Tiefenthaler <at@an-ti.eu>
|
||||
Andrew Munsell <andrew@wizardapps.net>
|
||||
Andy Rothfusz <github@metaliveblog.com>
|
||||
Andy Smith <github@anarkystic.com>
|
||||
Antony Messerli <amesserl@rackspace.com>
|
||||
@@ -14,7 +16,9 @@ Brandon Liu <bdon@bdon.org>
|
||||
Brian McCallister <brianm@skife.org>
|
||||
Bruno Bigras <bigras.bruno@gmail.com>
|
||||
Caleb Spare <cespare@gmail.com>
|
||||
Calen Pennington <cale@edx.org>
|
||||
Charles Hooper <charles.hooper@dotcloud.com>
|
||||
Christopher Currie <codemonkey+github@gmail.com>
|
||||
Daniel Gasienica <daniel@gasienica.ch>
|
||||
Daniel Mizyrycki <daniel.mizyrycki@dotcloud.com>
|
||||
Daniel Robinson <gottagetmac@gmail.com>
|
||||
@@ -22,11 +26,15 @@ Daniel Von Fange <daniel@leancoder.com>
|
||||
Dominik Honnef <dominik@honnef.co>
|
||||
Don Spaulding <donspauldingii@gmail.com>
|
||||
Dr Nic Williams <drnicwilliams@gmail.com>
|
||||
Elias Probst <mail@eliasprobst.eu>
|
||||
Eric Hanchrow <ehanchrow@ine.com>
|
||||
Evan Wies <evan@neomantra.net>
|
||||
Eric Myhre <hash@exultant.us>
|
||||
ezbercih <cem.ezberci@gmail.com>
|
||||
Flavio Castelli <fcastelli@suse.com>
|
||||
Francisco Souza <f@souza.cc>
|
||||
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
|
||||
Gareth Rushgrove <gareth@morethanseven.net>
|
||||
Guillaume J. Charmes <guillaume.charmes@dotcloud.com>
|
||||
Harley Laue <losinggeneration@gmail.com>
|
||||
Hunter Blanks <hunter@twilio.com>
|
||||
@@ -34,14 +42,22 @@ Jeff Lindsay <progrium@gmail.com>
|
||||
Jeremy Grosser <jeremy@synack.me>
|
||||
Joffrey F <joffrey@dotcloud.com>
|
||||
John Costa <john.costa@gmail.com>
|
||||
Jon Wedaman <jweede@gmail.com>
|
||||
Jonas Pfenniger <jonas@pfenniger.name>
|
||||
Jonathan Rudenberg <jonathan@titanous.com>
|
||||
Joseph Anthony Pasquale Holsten <joseph@josephholsten.com>
|
||||
Julien Barbier <write0@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@dotcloud.com>
|
||||
Ken Cochrane <kencochrane@gmail.com>
|
||||
Kevin J. Lynagh <kevin@keminglabs.com>
|
||||
kim0 <email.ahmedkamal@googlemail.com>
|
||||
Kiran Gangadharan <kiran.daredevil@gmail.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Marcus Farkas <toothlessgear@finitebox.com>
|
||||
Mark McGranaghan <mmcgrana@gmail.com>
|
||||
Maxim Treskin <zerthurd@gmail.com>
|
||||
meejah <meejah@meejah.ca>
|
||||
Michael Crosby <crosby.michael@gmail.com>
|
||||
Mikhail Sobolev <mss@mawhrin.net>
|
||||
Nate Jones <nate@endot.org>
|
||||
Nelson Chen <crazysim@gmail.com>
|
||||
@@ -49,18 +65,25 @@ Niall O'Higgins <niallo@unworkable.org>
|
||||
odk- <github@odkurzacz.org>
|
||||
Paul Bowsher <pbowsher@globalpersonals.co.uk>
|
||||
Paul Hammond <paul@paulhammond.org>
|
||||
Phil Spitler <pspitler@gmail.com>
|
||||
Piotr Bogdan <ppbogdan@gmail.com>
|
||||
Renato Riccieri Santos Zannon <renato.riccieri@gmail.com>
|
||||
Robert Obryk <robryk@gmail.com>
|
||||
Roberto Hashioka <roberto_hashioka@hotmail.com>
|
||||
Sam Alba <sam.alba@gmail.com>
|
||||
Sam J Sharpe <sam.sharpe@digital.cabinet-office.gov.uk>
|
||||
Shawn Siefkas <shawn.siefkas@meredith.com>
|
||||
Silas Sewell <silas@sewell.org>
|
||||
Solomon Hykes <solomon@dotcloud.com>
|
||||
Sridhar Ratnakumar <sridharr@activestate.com>
|
||||
Thatcher Peskens <thatcher@dotcloud.com>
|
||||
Thomas Bikeev <thomas.bikeev@mac.com>
|
||||
Thomas Hansen <thomas.hansen@gmail.com>
|
||||
Tianon Gravi <admwiggin@gmail.com>
|
||||
Tim Terhorst <mynamewastaken+git@gmail.com>
|
||||
Troy Howard <thoward37@gmail.com>
|
||||
Tobias Bieniek <Tobias.Bieniek@gmx.de>
|
||||
unclejack <unclejacksons@gmail.com>
|
||||
Victor Vieux <victor.vieux@dotcloud.com>
|
||||
Vivek Agarwal <me@vivek.im>
|
||||
Walter Stanish <walter@pratyeka.org>
|
||||
Will Dietz <w@wdtz.org>
|
||||
|
||||
@@ -1,5 +1,75 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.8 (2013-07-01)
|
||||
+ Builder: New build operation ENTRYPOINT adds an executable entry point to the container.
|
||||
- Runtime: Fix a bug which caused 'docker run -d' to no longer print the container ID.
|
||||
- Tests: Fix issues in the test suite
|
||||
|
||||
## 0.4.7 (2013-06-28)
|
||||
* Registry: easier push/pull to a custom registry
|
||||
* Remote API: the progress bar updates faster when downloading and uploading large files
|
||||
- Remote API: fix a bug in the optional unix socket transport
|
||||
* Runtime: improve detection of kernel version
|
||||
+ Runtime: host directories can be mounted as volumes with 'docker run -b'
|
||||
- Runtime: fix an issue when only attaching to stdin
|
||||
* Runtime: use 'tar --numeric-owner' to avoid uid mismatch across multiple hosts
|
||||
* Hack: improve test suite and dev environment
|
||||
* Hack: remove dependency on unit tests on 'os/user'
|
||||
+ Documentation: add terminology section
|
||||
|
||||
## 0.4.6 (2013-06-22)
|
||||
- Runtime: fix a bug which caused creation of empty images (and volumes) to crash.
|
||||
|
||||
## 0.4.5 (2013-06-21)
|
||||
+ Builder: 'docker build git://URL' fetches and builds a remote git repository
|
||||
* Runtime: 'docker ps -s' optionally prints container size
|
||||
* Tests: Improved and simplified
|
||||
- Runtime: fix a regression introduced in 0.4.3 which caused the logs command to fail.
|
||||
- Builder: fix a regression when using ADD with single regular file.
|
||||
|
||||
## 0.4.4 (2013-06-19)
|
||||
- Builder: fix a regression introduced in 0.4.3 which caused builds to fail on new clients.
|
||||
|
||||
## 0.4.3 (2013-06-19)
|
||||
+ Builder: ADD of a local file will detect tar archives and unpack them
|
||||
* Runtime: Remove bsdtar dependency
|
||||
* Runtime: Add unix socket and multiple -H support
|
||||
* Runtime: Prevent rm of running containers
|
||||
* Runtime: Use go1.1 cookiejar
|
||||
* Builder: ADD improvements: use tar for copy + automatically unpack local archives
|
||||
* Builder: ADD uses tar/untar for copies instead of calling 'cp -ar'
|
||||
* Builder: nicer output for 'docker build'
|
||||
* Builder: fixed the behavior of ADD to be (mostly) reverse-compatible, predictable and well-documented.
|
||||
* Client: HumanReadable ProgressBar sizes in pull
|
||||
* Client: Fix docker version's git commit output
|
||||
* API: Send all tags on History API call
|
||||
* API: Add tag lookup to history command. Fixes #882
|
||||
- Runtime: Fix issue detaching from running TTY container
|
||||
- Runtime: Forbid parralel push/pull for a single image/repo. Fixes #311
|
||||
- Runtime: Fix race condition within Run command when attaching.
|
||||
- Builder: fix a bug which caused builds to fail if ADD was the first command
|
||||
- Documentation: fix missing command in irc bouncer example
|
||||
|
||||
## 0.4.2 (2013-06-17)
|
||||
- Packaging: Bumped version to work around an Ubuntu bug
|
||||
|
||||
## 0.4.1 (2013-06-17)
|
||||
+ Remote Api: Add flag to enable cross domain requests
|
||||
+ Remote Api/Client: Add images and containers sizes in docker ps and docker images
|
||||
+ Runtime: Configure dns configuration host-wide with 'docker -d -dns'
|
||||
+ Runtime: Detect faulty DNS configuration and replace it with a public default
|
||||
+ Runtime: allow docker run <name>:<id>
|
||||
+ Runtime: you can now specify public port (ex: -p 80:4500)
|
||||
* Client: allow multiple params in inspect
|
||||
* Client: Print the container id before the hijack in `docker run`
|
||||
* Registry: add regexp check on repo's name
|
||||
* Registry: Move auth to the client
|
||||
* Runtime: improved image removal to garbage-collect unreferenced parents
|
||||
* Vagrantfile: Add the rest api port to vagrantfile's port_forward
|
||||
* Upgrade to Go 1.1
|
||||
- Builder: don't ignore last line in Dockerfile when it doesn't end with \n
|
||||
- Registry: Remove login check on pull
|
||||
|
||||
## 0.4.0 (2013-06-03)
|
||||
+ Introducing Builder: 'docker build' builds a container, layer by layer, from a source repository containing a Dockerfile
|
||||
+ Introducing Remote API: control Docker programmatically using a simple HTTP/json API
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
## FIXME
|
||||
|
||||
This file is a loose collection of things to improve in the codebase, for the internal
|
||||
use of the maintainers.
|
||||
|
||||
They are not big enough to be in the roadmap, not user-facing enough to be github issues,
|
||||
and not important enough to be discussed in the mailing list.
|
||||
|
||||
They are just like FIXME comments in the source code, except we're not sure where in the source
|
||||
to put them - so we put them here :)
|
||||
|
||||
|
||||
* Merge Runtime, Server and Builder into Runtime
|
||||
* Run linter on codebase
|
||||
* Unify build commands and regular commands
|
||||
* Move source code into src/ subdir for clarity
|
||||
* Clean up the Makefile, it's a mess
|
||||
* docker build: on non-existent local path for ADD, don't show full absolute path on the host
|
||||
* mount into /dockerinit rather than /sbin/init
|
||||
* docker tag foo REPO:TAG
|
||||
* use size header for progress bar in pull
|
||||
* Clean up context upload in build!!!
|
||||
* Parallel pull
|
||||
* Ensure /proc/sys/net/ipv4/ip_forward is 1
|
||||
* Force DNS to public!
|
||||
* Always generate a resolv.conf per container, to avoid changing resolv.conf under thne container's feet
|
||||
* Save metadata with import/export
|
||||
* Upgrade dockerd without stopping containers
|
||||
* bring back git revision info, looks like it was lost
|
||||
* Simple command to remove all untagged images
|
||||
* Simple command to clean up containers for disk space
|
||||
* Caching after an ADD
|
||||
* entry point config
|
||||
* bring back git revision info, looks like it was lost
|
||||
* Clean up the ProgressReader api, it's a PITA to use
|
||||
@@ -2,6 +2,8 @@ DOCKER_PACKAGE := github.com/dotcloud/docker
|
||||
RELEASE_VERSION := $(shell git tag | grep -E "v[0-9\.]+$$" | sort -nr | head -n 1)
|
||||
SRCRELEASE := docker-$(RELEASE_VERSION)
|
||||
BINRELEASE := docker-$(RELEASE_VERSION).tgz
|
||||
BUILD_SRC := build_src
|
||||
BUILD_PATH := ${BUILD_SRC}/src/${DOCKER_PACKAGE}
|
||||
|
||||
GIT_ROOT := $(shell git rev-parse --show-toplevel)
|
||||
BUILD_DIR := $(CURDIR)/.gopath
|
||||
@@ -17,7 +19,7 @@ endif
|
||||
GIT_COMMIT = $(shell git rev-parse --short HEAD)
|
||||
GIT_STATUS = $(shell test -n "`git status --porcelain`" && echo "+CHANGES")
|
||||
|
||||
BUILD_OPTIONS = -ldflags "-X main.GIT_COMMIT $(GIT_COMMIT)$(GIT_STATUS)"
|
||||
BUILD_OPTIONS = -a -ldflags "-X main.GITCOMMIT $(GIT_COMMIT)$(GIT_STATUS) -d -w"
|
||||
|
||||
SRC_DIR := $(GOPATH)/src
|
||||
|
||||
@@ -33,7 +35,7 @@ all: $(DOCKER_BIN)
|
||||
|
||||
$(DOCKER_BIN): $(DOCKER_DIR)
|
||||
@mkdir -p $(dir $@)
|
||||
@(cd $(DOCKER_MAIN); go build $(GO_OPTIONS) $(BUILD_OPTIONS) -o $@)
|
||||
@(cd $(DOCKER_MAIN); CGO_ENABLED=0 go build $(GO_OPTIONS) $(BUILD_OPTIONS) -o $@)
|
||||
@echo $(DOCKER_BIN_RELATIVE) is created.
|
||||
|
||||
$(DOCKER_DIR):
|
||||
@@ -46,6 +48,7 @@ whichrelease:
|
||||
|
||||
release: $(BINRELEASE)
|
||||
s3cmd -P put $(BINRELEASE) s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-$(RELEASE_VERSION).tgz
|
||||
s3cmd -P put docker-latest.tgz s3://get.docker.io/builds/`uname -s`/`uname -m`/docker-latest.tgz
|
||||
|
||||
srcrelease: $(SRCRELEASE)
|
||||
deps: $(DOCKER_DIR)
|
||||
@@ -60,6 +63,7 @@ $(SRCRELEASE):
|
||||
$(BINRELEASE): $(SRCRELEASE)
|
||||
rm -f $(BINRELEASE)
|
||||
cd $(SRCRELEASE); make; cp -R bin docker-$(RELEASE_VERSION); tar -f ../$(BINRELEASE) -zv -c docker-$(RELEASE_VERSION)
|
||||
cd $(SRCRELEASE); cp -R bin docker-latest; tar -f ../docker-latest.tgz -zv -c docker-latest
|
||||
|
||||
clean:
|
||||
@rm -rf $(dir $(DOCKER_BIN))
|
||||
@@ -69,8 +73,16 @@ else ifneq ($(DOCKER_DIR), $(realpath $(DOCKER_DIR)))
|
||||
@rm -f $(DOCKER_DIR)
|
||||
endif
|
||||
|
||||
test: all
|
||||
@(cd $(DOCKER_DIR); sudo -E go test $(GO_OPTIONS))
|
||||
test:
|
||||
# Copy docker source and dependencies for testing
|
||||
rm -rf ${BUILD_SRC}; mkdir -p ${BUILD_PATH}
|
||||
tar --exclude=${BUILD_SRC} -cz . | tar -xz -C ${BUILD_PATH}
|
||||
GOPATH=${CURDIR}/${BUILD_SRC} go get -d
|
||||
# Do the test
|
||||
sudo -E GOPATH=${CURDIR}/${BUILD_SRC} go test ${GO_OPTIONS}
|
||||
|
||||
testall: all
|
||||
@(cd $(DOCKER_DIR); sudo -E go test ./... $(GO_OPTIONS))
|
||||
|
||||
fmt:
|
||||
@gofmt -s -l -w .
|
||||
|
||||
@@ -3,4 +3,9 @@ Copyright 2012-2013 dotCloud, inc.
|
||||
|
||||
This product includes software developed at dotCloud, inc. (http://www.dotcloud.com).
|
||||
|
||||
This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
|
||||
This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
|
||||
|
||||
Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
|
||||
legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
|
||||
and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
|
||||
Department of Commerce.
|
||||
|
||||
@@ -97,7 +97,7 @@ Quick install on Ubuntu 12.04 and 12.10
|
||||
---------------------------------------
|
||||
|
||||
```bash
|
||||
curl get.docker.io | sh -x
|
||||
curl get.docker.io | sudo sh -x
|
||||
```
|
||||
|
||||
Binary installs
|
||||
@@ -108,7 +108,7 @@ Note that some methods are community contributions and not yet officially suppor
|
||||
|
||||
* [Ubuntu 12.04 and 12.10 (officially supported)](http://docs.docker.io/en/latest/installation/ubuntulinux/)
|
||||
* [Arch Linux](http://docs.docker.io/en/latest/installation/archlinux/)
|
||||
* [MacOS X (with Vagrant)](http://docs.docker.io/en/latest/installation/macos/)
|
||||
* [Mac OS X (with Vagrant)](http://docs.docker.io/en/latest/installation/vagrant/)
|
||||
* [Windows (with Vagrant)](http://docs.docker.io/en/latest/installation/windows/)
|
||||
* [Amazon EC2 (with Vagrant)](http://docs.docker.io/en/latest/installation/amazon/)
|
||||
|
||||
@@ -181,7 +181,7 @@ Running an irc bouncer
|
||||
----------------------
|
||||
|
||||
```bash
|
||||
BOUNCER_ID=$(docker run -d -p 6667 -u irc shykes/znc $USER $PASSWORD)
|
||||
BOUNCER_ID=$(docker run -d -p 6667 -u irc shykes/znc zncrun $USER $PASSWORD)
|
||||
echo "Configure your irc client to connect to port $(docker port $BOUNCER_ID 6667) of this machine"
|
||||
```
|
||||
|
||||
@@ -216,7 +216,8 @@ PORT=$(docker port $JOB 4444)
|
||||
|
||||
# Connect to the public port via the host's public address
|
||||
# Please note that because of how routing works connecting to localhost or 127.0.0.1 $PORT will not work.
|
||||
IP=$(ifconfig eth0 | perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }')
|
||||
# Replace *eth0* according to your local interface name.
|
||||
IP=$(ip -o -4 addr list eth0 | perl -n -e 'if (m{inet\s([\d\.]+)\/\d+\s}xms) { print $1 }')
|
||||
echo hello world | nc $IP $PORT
|
||||
|
||||
# Verify that the network connection worked
|
||||
@@ -251,7 +252,7 @@ Note
|
||||
----
|
||||
|
||||
We also keep the documentation in this repository. The website documentation is generated using sphinx using these sources.
|
||||
Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/master/docs/README.md
|
||||
Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/tree/master/docs/README.md
|
||||
|
||||
Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome.
|
||||
|
||||
@@ -262,14 +263,14 @@ Setting up a dev environment
|
||||
Instructions that have been verified to work on Ubuntu 12.10,
|
||||
|
||||
```bash
|
||||
sudo apt-get -y install lxc wget bsdtar curl golang git
|
||||
sudo apt-get -y install lxc curl xz-utils golang git
|
||||
|
||||
export GOPATH=~/go/
|
||||
export PATH=$GOPATH/bin:$PATH
|
||||
|
||||
mkdir -p $GOPATH/src/github.com/dotcloud
|
||||
cd $GOPATH/src/github.com/dotcloud
|
||||
git clone git@github.com:dotcloud/docker.git
|
||||
git clone https://github.com/dotcloud/docker.git
|
||||
cd docker
|
||||
|
||||
go get -v github.com/dotcloud/docker/...
|
||||
@@ -371,4 +372,10 @@ Standard Container Specification
|
||||
|
||||
#### Security
|
||||
|
||||
### Legal
|
||||
|
||||
Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
|
||||
legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
|
||||
and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
|
||||
Department of Commerce.
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
|
||||
BOX_NAME = ENV['BOX_NAME'] || "ubuntu"
|
||||
BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64.box"
|
||||
VF_BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64_vmware_fusion.box"
|
||||
AWS_REGION = ENV['AWS_REGION'] || "us-east-1"
|
||||
AWS_AMI = ENV['AWS_AMI'] || "ami-d0f89fb9"
|
||||
FORWARD_DOCKER_PORTS = ENV['FORWARD_DOCKER_PORTS']
|
||||
|
||||
Vagrant::Config.run do |config|
|
||||
# Setup virtual machine box. This VM configuration code is always executed.
|
||||
config.vm.box = BOX_NAME
|
||||
config.vm.box_url = BOX_URI
|
||||
config.vm.forward_port 4243, 4243
|
||||
|
||||
# Provision docker and new kernel if deployment was not done
|
||||
if Dir.glob("#{File.dirname(__FILE__)}/.vagrant/machines/default/*/id").empty?
|
||||
@@ -65,8 +68,29 @@ Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config|
|
||||
rs.image = /Ubuntu/
|
||||
end
|
||||
|
||||
config.vm.provider :vmware_fusion do |f, override|
|
||||
override.vm.box = BOX_NAME
|
||||
override.vm.box_url = VF_BOX_URI
|
||||
override.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
f.vmx["displayName"] = "docker"
|
||||
end
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
config.vm.box = BOX_NAME
|
||||
config.vm.box_url = BOX_URI
|
||||
end
|
||||
end
|
||||
|
||||
if !FORWARD_DOCKER_PORTS.nil?
|
||||
Vagrant::VERSION < "1.1.0" and Vagrant::Config.run do |config|
|
||||
(49000..49900).each do |port|
|
||||
config.vm.forward_port port, port
|
||||
end
|
||||
end
|
||||
|
||||
Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config|
|
||||
(49000..49900).each do |port|
|
||||
config.vm.network :forwarded_port, :host => port, :guest => port
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,13 +7,19 @@ import (
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const API_VERSION = 1.1
|
||||
const APIVERSION = 1.3
|
||||
const DEFAULTHTTPHOST string = "127.0.0.1"
|
||||
const DEFAULTHTTPPORT int = 4243
|
||||
|
||||
func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
@@ -45,32 +51,40 @@ func httpError(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
} else if strings.HasPrefix(err.Error(), "Bad parameter") {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if strings.HasPrefix(err.Error(), "Conflict") {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
} else if strings.HasPrefix(err.Error(), "Impossible") {
|
||||
http.Error(w, err.Error(), http.StatusNotAcceptable)
|
||||
} else if strings.HasPrefix(err.Error(), "Wrong login/password") {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
} else if strings.Contains(err.Error(), "hasn't been activated") {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJson(w http.ResponseWriter, b []byte) {
|
||||
func writeJSON(w http.ResponseWriter, b []byte) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// FIXME: Use stvconv.ParseBool() instead?
|
||||
func getBoolParam(value string) (bool, error) {
|
||||
if value == "1" || strings.ToLower(value) == "true" {
|
||||
return true, nil
|
||||
}
|
||||
if value == "" || value == "0" || strings.ToLower(value) == "false" {
|
||||
if value == "" {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("Bad parameter")
|
||||
ret, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Bad parameter")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// FIXME: Handle multiple login at once
|
||||
// FIXME: return specific error code if config file missing?
|
||||
if version > 1.1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
authConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
@@ -82,40 +96,45 @@ func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// FIXME: Handle multiple login at once
|
||||
config := &auth.AuthConfig{}
|
||||
if err := json.NewDecoder(r.Body).Decode(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
authConfig = &auth.AuthConfig{}
|
||||
}
|
||||
if config.Username == authConfig.Username {
|
||||
config.Password = authConfig.Password
|
||||
}
|
||||
|
||||
newAuthConfig := auth.NewAuthConfig(config.Username, config.Password, config.Email, srv.runtime.root)
|
||||
status, err := auth.Login(newAuthConfig)
|
||||
authConfig := &auth.AuthConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
b, err := json.Marshal(&ApiAuth{Status: status})
|
||||
status := ""
|
||||
if version > 1.1 {
|
||||
status, err = auth.Login(authConfig, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if authConfig.Username == localAuthConfig.Username {
|
||||
authConfig.Password = localAuthConfig.Password
|
||||
}
|
||||
|
||||
newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root)
|
||||
status, err = auth.Login(newAuthConfig, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if status != "" {
|
||||
b, err := json.Marshal(&APIAuth{Status: status})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@@ -128,7 +147,7 @@ func getVersion(srv *Server, version float64, w http.ResponseWriter, r *http.Req
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,7 +176,7 @@ func getContainersExport(srv *Server, version float64, w http.ResponseWriter, r
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImagesJson(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
func getImagesJSON(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,7 +195,7 @@ func getImagesJson(srv *Server, version float64, w http.ResponseWriter, r *http.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -193,7 +212,7 @@ func getInfo(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -210,7 +229,7 @@ func getImagesHistory(srv *Server, version float64, w http.ResponseWriter, r *ht
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,11 +246,11 @@ func getContainersChanges(srv *Server, version float64, w http.ResponseWriter, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getContainersJson(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
func getContainersJSON(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -239,6 +258,10 @@ func getContainersJson(srv *Server, version float64, w http.ResponseWriter, r *h
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size, err := getBoolParam(r.Form.Get("size"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
since := r.Form.Get("since")
|
||||
before := r.Form.Get("before")
|
||||
n, err := strconv.Atoi(r.Form.Get("limit"))
|
||||
@@ -246,12 +269,12 @@ func getContainersJson(srv *Server, version float64, w http.ResponseWriter, r *h
|
||||
n = -1
|
||||
}
|
||||
|
||||
outs := srv.Containers(all, n, since, before)
|
||||
outs := srv.Containers(all, size, n, since, before)
|
||||
b, err := json.Marshal(outs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -294,12 +317,12 @@ func postCommit(srv *Server, version float64, w http.ResponseWriter, r *http.Req
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(&ApiId{id})
|
||||
b, err := json.Marshal(&APIID{id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -320,7 +343,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
|
||||
sf := utils.NewStreamFormatter(version > 1.0)
|
||||
if image != "" { //pull
|
||||
registry := r.Form.Get("registry")
|
||||
if err := srv.ImagePull(image, tag, registry, w, sf); err != nil {
|
||||
if err := srv.ImagePull(image, tag, registry, w, sf, &auth.AuthConfig{}); err != nil {
|
||||
if sf.Used() {
|
||||
w.Write(sf.FormatError(err))
|
||||
return nil
|
||||
@@ -353,7 +376,7 @@ func getImagesSearch(srv *Server, version float64, w http.ResponseWriter, r *htt
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -372,22 +395,34 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
sf := utils.NewStreamFormatter(version > 1.0)
|
||||
imgId, err := srv.ImageInsert(name, url, path, w, sf)
|
||||
imgID, err := srv.ImageInsert(name, url, path, w, sf)
|
||||
if err != nil {
|
||||
if sf.Used() {
|
||||
w.Write(sf.FormatError(err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(&ApiId{Id: imgId})
|
||||
b, err := json.Marshal(&APIID{ID: imgID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
authConfig := &auth.AuthConfig{}
|
||||
if version > 1.1 {
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil && err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
authConfig = localAuthConfig
|
||||
}
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -401,7 +436,7 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
sf := utils.NewStreamFormatter(version > 1.0)
|
||||
if err := srv.ImagePush(name, registry, w, sf); err != nil {
|
||||
if err := srv.ImagePush(name, registry, w, sf, authConfig); err != nil {
|
||||
if sf.Used() {
|
||||
w.Write(sf.FormatError(err))
|
||||
return nil
|
||||
@@ -413,17 +448,23 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http
|
||||
|
||||
func postContainersCreate(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
config := &Config{}
|
||||
out := &APIRun{}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(config.Dns) == 0 && len(srv.runtime.Dns) == 0 && utils.CheckLocalDns() {
|
||||
out.Warnings = append(out.Warnings, fmt.Sprintf("Docker detected local DNS server on resolv.conf. Using default external servers: %v", defaultDns))
|
||||
config.Dns = defaultDns
|
||||
}
|
||||
|
||||
id, err := srv.ContainerCreate(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.ID = id
|
||||
|
||||
out := &ApiRun{
|
||||
Id: id,
|
||||
}
|
||||
if config.Memory > 0 && !srv.runtime.capabilities.MemoryLimit {
|
||||
log.Println("WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.")
|
||||
out.Warnings = append(out.Warnings, "Your kernel does not support memory limit capabilities. Limitation discarded.")
|
||||
@@ -432,12 +473,13 @@ func postContainersCreate(srv *Server, version float64, w http.ResponseWriter, r
|
||||
log.Println("WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.")
|
||||
out.Warnings = append(out.Warnings, "Your kernel does not support memory swap capabilities. Limitation discarded.")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -481,23 +523,50 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if vars == nil {
|
||||
return fmt.Errorf("Missing parameter")
|
||||
}
|
||||
name := vars["name"]
|
||||
if err := srv.ImageDelete(name); err != nil {
|
||||
imgs, err := srv.ImageDelete(name, version > 1.1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
if imgs != nil {
|
||||
if len(imgs) != 0 {
|
||||
b, err := json.Marshal(imgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, b)
|
||||
} else {
|
||||
return fmt.Errorf("Conflict, %s wasn't deleted", name)
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postContainersStart(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
hostConfig := &HostConfig{}
|
||||
|
||||
// allow a nil body for backwards compatibility
|
||||
if r.Body != nil {
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(hostConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if vars == nil {
|
||||
return fmt.Errorf("Missing parameter")
|
||||
}
|
||||
name := vars["name"]
|
||||
if err := srv.ContainerStart(name); err != nil {
|
||||
if err := srv.ContainerStart(name, hostConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@@ -534,11 +603,11 @@ func postContainersWait(srv *Server, version float64, w http.ResponseWriter, r *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(&ApiWait{StatusCode: status})
|
||||
b, err := json.Marshal(&APIWait{StatusCode: status})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -602,7 +671,20 @@ func postContainersAttach(srv *Server, version float64, w http.ResponseWriter, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
defer func() {
|
||||
if tcpc, ok := in.(*net.TCPConn); ok {
|
||||
tcpc.CloseWrite()
|
||||
} else {
|
||||
in.Close()
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
if tcpc, ok := out.(*net.TCPConn); ok {
|
||||
tcpc.CloseWrite()
|
||||
} else if closer, ok := out.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
|
||||
if err := srv.ContainerAttach(name, logs, stream, stdin, stdout, stderr, in, out); err != nil {
|
||||
@@ -625,7 +707,7 @@ func getContainersByName(srv *Server, version float64, w http.ResponseWriter, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -643,17 +725,17 @@ func getImagesByName(srv *Server, version float64, w http.ResponseWriter, r *htt
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func postImagesGetCache(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
apiConfig := &ApiImageConfig{}
|
||||
apiConfig := &APIImageConfig{}
|
||||
if err := json.NewDecoder(r.Body).Decode(apiConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image, err := srv.ImageGetCached(apiConfig.Id, apiConfig.Config)
|
||||
image, err := srv.ImageGetCached(apiConfig.ID, apiConfig.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -661,64 +743,104 @@ func postImagesGetCache(srv *Server, version float64, w http.ResponseWriter, r *
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
apiId := &ApiId{Id: image.Id}
|
||||
b, err := json.Marshal(apiId)
|
||||
apiID := &APIID{ID: image.ID}
|
||||
b, err := json.Marshal(apiID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJson(w, b)
|
||||
writeJSON(w, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := r.ParseMultipartForm(4096); err != nil {
|
||||
return err
|
||||
if version < 1.3 {
|
||||
return fmt.Errorf("Multipart upload for build is no longer supported. Please upgrade your docker client.")
|
||||
}
|
||||
remote := r.FormValue("t")
|
||||
remoteURL := r.FormValue("remote")
|
||||
repoName := r.FormValue("t")
|
||||
tag := ""
|
||||
if strings.Contains(remote, ":") {
|
||||
remoteParts := strings.Split(remote, ":")
|
||||
if strings.Contains(repoName, ":") {
|
||||
remoteParts := strings.Split(repoName, ":")
|
||||
tag = remoteParts[1]
|
||||
remote = remoteParts[0]
|
||||
repoName = remoteParts[0]
|
||||
}
|
||||
|
||||
dockerfile, _, err := r.FormFile("Dockerfile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var context io.Reader
|
||||
|
||||
context, _, err := r.FormFile("Context")
|
||||
if err != nil {
|
||||
if err != http.ErrMissingFile {
|
||||
if remoteURL == "" {
|
||||
context = r.Body
|
||||
} else if utils.IsGIT(remoteURL) {
|
||||
if !strings.HasPrefix(remoteURL, "git://") {
|
||||
remoteURL = "https://" + remoteURL
|
||||
}
|
||||
root, err := ioutil.TempDir("", "docker-build-git")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
if output, err := exec.Command("git", "clone", remoteURL, root).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("Error trying to use git: %s (%s)", err, output)
|
||||
}
|
||||
|
||||
c, err := Tar(root, Bzip2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
context = c
|
||||
} else if utils.IsURL(remoteURL) {
|
||||
f, err := utils.Download(remoteURL, ioutil.Discard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Body.Close()
|
||||
dockerFile, err := ioutil.ReadAll(f.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := mkBuildContext(string(dockerFile), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
context = c
|
||||
}
|
||||
b := NewBuildFile(srv, utils.NewWriteFlusher(w))
|
||||
if id, err := b.Build(dockerfile, context); err != nil {
|
||||
id, err := b.Build(context)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error build: %s\n", err)
|
||||
} else if remote != "" {
|
||||
srv.runtime.repositories.Set(remote, tag, id, false)
|
||||
return err
|
||||
}
|
||||
if repoName != "" {
|
||||
srv.runtime.repositories.Set(repoName, tag, id, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListenAndServe(addr string, srv *Server, logging bool) error {
|
||||
func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
func writeCorsHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
|
||||
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
||||
}
|
||||
|
||||
func createRouter(srv *Server, logging bool) (*mux.Router, error) {
|
||||
r := mux.NewRouter()
|
||||
log.Printf("Listening for HTTP on %s\n", addr)
|
||||
|
||||
m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{
|
||||
"GET": {
|
||||
"/auth": getAuth,
|
||||
"/version": getVersion,
|
||||
"/info": getInfo,
|
||||
"/images/json": getImagesJson,
|
||||
"/images/json": getImagesJSON,
|
||||
"/images/viz": getImagesViz,
|
||||
"/images/search": getImagesSearch,
|
||||
"/images/{name:.*}/history": getImagesHistory,
|
||||
"/images/{name:.*}/json": getImagesByName,
|
||||
"/containers/ps": getContainersJson,
|
||||
"/containers/json": getContainersJson,
|
||||
"/containers/ps": getContainersJSON,
|
||||
"/containers/json": getContainersJSON,
|
||||
"/containers/{name:.*}/export": getContainersExport,
|
||||
"/containers/{name:.*}/changes": getContainersChanges,
|
||||
"/containers/{name:.*}/json": getContainersByName,
|
||||
@@ -745,6 +867,9 @@ func ListenAndServe(addr string, srv *Server, logging bool) error {
|
||||
"/containers/{name:.*}": deleteContainers,
|
||||
"/images/{name:.*}": deleteImages,
|
||||
},
|
||||
"OPTIONS": {
|
||||
"": optionsHandler,
|
||||
},
|
||||
}
|
||||
|
||||
for method, routes := range m {
|
||||
@@ -756,6 +881,7 @@ func ListenAndServe(addr string, srv *Server, logging bool) error {
|
||||
localFct := fct
|
||||
f := func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Debugf("Calling %s %s", localMethod, localRoute)
|
||||
|
||||
if logging {
|
||||
log.Println(r.Method, r.RequestURI)
|
||||
}
|
||||
@@ -767,19 +893,47 @@ func ListenAndServe(addr string, srv *Server, logging bool) error {
|
||||
}
|
||||
version, err := strconv.ParseFloat(mux.Vars(r)["version"], 64)
|
||||
if err != nil {
|
||||
version = API_VERSION
|
||||
version = APIVERSION
|
||||
}
|
||||
if version == 0 || version > API_VERSION {
|
||||
if srv.enableCors {
|
||||
writeCorsHeaders(w, r)
|
||||
}
|
||||
if version == 0 || version > APIVERSION {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := localFct(srv, version, w, r, mux.Vars(r)); err != nil {
|
||||
httpError(w, err)
|
||||
}
|
||||
}
|
||||
r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f)
|
||||
r.Path(localRoute).Methods(localMethod).HandlerFunc(f)
|
||||
|
||||
if localRoute == "" {
|
||||
r.Methods(localMethod).HandlerFunc(f)
|
||||
} else {
|
||||
r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f)
|
||||
r.Path(localRoute).Methods(localMethod).HandlerFunc(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
return http.ListenAndServe(addr, r)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func ListenAndServe(proto, addr string, srv *Server, logging bool) error {
|
||||
log.Printf("Listening for HTTP on %s (%s)\n", addr, proto)
|
||||
|
||||
r, err := createRouter(srv, logging)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l, e := net.Listen(proto, addr)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
//as the daemon is launched as root, change to permission of the socket to allow non-root to connect
|
||||
if proto == "unix" {
|
||||
os.Chmod(addr, 0777)
|
||||
}
|
||||
httpSrv := http.Server{Addr: addr, Handler: r}
|
||||
return httpSrv.Serve(l)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package docker
|
||||
|
||||
type ApiHistory struct {
|
||||
Id string
|
||||
type APIHistory struct {
|
||||
ID string `json:"Id"`
|
||||
Tags []string `json:",omitempty"`
|
||||
Created int64
|
||||
CreatedBy string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ApiImages struct {
|
||||
Repository string `json:",omitempty"`
|
||||
Tag string `json:",omitempty"`
|
||||
Id string
|
||||
Created int64
|
||||
type APIImages struct {
|
||||
Repository string `json:",omitempty"`
|
||||
Tag string `json:",omitempty"`
|
||||
ID string `json:"Id"`
|
||||
Created int64
|
||||
Size int64
|
||||
VirtualSize int64
|
||||
}
|
||||
|
||||
type ApiInfo struct {
|
||||
type APIInfo struct {
|
||||
Debug bool
|
||||
Containers int
|
||||
Images int
|
||||
@@ -23,48 +26,55 @@ type ApiInfo struct {
|
||||
SwapLimit bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ApiContainers struct {
|
||||
Id string
|
||||
Image string
|
||||
Command string
|
||||
Created int64
|
||||
Status string
|
||||
Ports string
|
||||
type APIRmi struct {
|
||||
Deleted string `json:",omitempty"`
|
||||
Untagged string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ApiSearch struct {
|
||||
type APIContainers struct {
|
||||
ID string `json:"Id"`
|
||||
Image string
|
||||
Command string
|
||||
Created int64
|
||||
Status string
|
||||
Ports string
|
||||
SizeRw int64
|
||||
SizeRootFs int64
|
||||
}
|
||||
|
||||
type APISearch struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type ApiId struct {
|
||||
Id string
|
||||
type APIID struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
|
||||
type ApiRun struct {
|
||||
Id string
|
||||
type APIRun struct {
|
||||
ID string `json:"Id"`
|
||||
Warnings []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ApiPort struct {
|
||||
type APIPort struct {
|
||||
Port string
|
||||
}
|
||||
|
||||
type ApiVersion struct {
|
||||
type APIVersion struct {
|
||||
Version string
|
||||
GitCommit string `json:",omitempty"`
|
||||
GoVersion string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type ApiWait struct {
|
||||
type APIWait struct {
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
type ApiAuth struct {
|
||||
type APIAuth struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
type ApiImageConfig struct {
|
||||
Id string
|
||||
type APIImageConfig struct {
|
||||
ID string `json:"Id"`
|
||||
*Config
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/dotcloud/docker/auth"
|
||||
"github.com/dotcloud/docker/registry"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"net"
|
||||
@@ -18,7 +17,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetAuth(t *testing.T) {
|
||||
func TestGetBoolParam(t *testing.T) {
|
||||
if ret, err := getBoolParam("true"); err != nil || !ret {
|
||||
t.Fatalf("true -> true, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam("True"); err != nil || !ret {
|
||||
t.Fatalf("True -> true, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam("1"); err != nil || !ret {
|
||||
t.Fatalf("1 -> true, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam(""); err != nil || ret {
|
||||
t.Fatalf("\"\" -> false, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam("false"); err != nil || ret {
|
||||
t.Fatalf("false -> false, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam("0"); err != nil || ret {
|
||||
t.Fatalf("0 -> false, nil | got %t %s", ret, err)
|
||||
}
|
||||
if ret, err := getBoolParam("faux"); err == nil || ret {
|
||||
t.Fatalf("faux -> false, err | got %t %s", ret, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAuth(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -37,29 +60,23 @@ func TestGetAuth(t *testing.T) {
|
||||
Email: "utest@yopmail.com",
|
||||
}
|
||||
|
||||
authConfigJson, err := json.Marshal(authConfig)
|
||||
authConfigJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/auth", bytes.NewReader(authConfigJson))
|
||||
req, err := http.NewRequest("POST", "/auth", bytes.NewReader(authConfigJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := postAuth(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := postAuth(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r.Code != http.StatusOK && r.Code != 0 {
|
||||
t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code)
|
||||
}
|
||||
|
||||
newAuthConfig := registry.NewRegistry(runtime.root).GetAuthConfig(false)
|
||||
if newAuthConfig.Username != authConfig.Username ||
|
||||
newAuthConfig.Email != authConfig.Email {
|
||||
t.Fatalf("The auth configuration hasn't been set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
@@ -73,11 +90,11 @@ func TestGetVersion(t *testing.T) {
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
if err := getVersion(srv, API_VERSION, r, nil, nil); err != nil {
|
||||
if err := getVersion(srv, APIVERSION, r, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v := &ApiVersion{}
|
||||
v := &APIVersion{}
|
||||
if err = json.Unmarshal(r.Body.Bytes(), v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -95,23 +112,28 @@ func TestGetInfo(t *testing.T) {
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
if err := getInfo(srv, API_VERSION, r, nil, nil); err != nil {
|
||||
initialImages, err := srv.runtime.graph.All()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
infos := &ApiInfo{}
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
if err := getInfo(srv, APIVERSION, r, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
infos := &APIInfo{}
|
||||
err = json.Unmarshal(r.Body.Bytes(), infos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if infos.Images != 1 {
|
||||
t.Errorf("Excepted images: %d, %d found", 1, infos.Images)
|
||||
if infos.Images != len(initialImages) {
|
||||
t.Errorf("Excepted images: %d, %d found", len(initialImages), infos.Images)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetImagesJson(t *testing.T) {
|
||||
func TestGetImagesJSON(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -120,6 +142,11 @@ func TestGetImagesJson(t *testing.T) {
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
initialImages, err := srv.Images(true, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// all=0
|
||||
req, err := http.NewRequest("GET", "/images/json?all=0", nil)
|
||||
if err != nil {
|
||||
@@ -128,21 +155,28 @@ func TestGetImagesJson(t *testing.T) {
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
if err := getImagesJson(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := getImagesJSON(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images := []ApiImages{}
|
||||
images := []APIImages{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &images); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 1 {
|
||||
t.Errorf("Excepted 1 image, %d found", len(images))
|
||||
if len(images) != len(initialImages) {
|
||||
t.Errorf("Excepted %d image, %d found", len(initialImages), len(images))
|
||||
}
|
||||
|
||||
if images[0].Repository != unitTestImageName {
|
||||
t.Errorf("Excepted image %s, %s found", unitTestImageName, images[0].Repository)
|
||||
found := false
|
||||
for _, img := range images {
|
||||
if img.Repository == unitTestImageName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Excepted image %s, %+v found", unitTestImageName, images)
|
||||
}
|
||||
|
||||
r2 := httptest.NewRecorder()
|
||||
@@ -153,42 +187,49 @@ func TestGetImagesJson(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := getImagesJson(srv, API_VERSION, r2, req2, nil); err != nil {
|
||||
if err := getImagesJSON(srv, APIVERSION, r2, req2, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images2 := []ApiImages{}
|
||||
images2 := []APIImages{}
|
||||
if err := json.Unmarshal(r2.Body.Bytes(), &images2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images2) != 1 {
|
||||
t.Errorf("Excepted 1 image, %d found", len(images2))
|
||||
if len(images2) != len(initialImages) {
|
||||
t.Errorf("Excepted %d image, %d found", len(initialImages), len(images2))
|
||||
}
|
||||
|
||||
if images2[0].Id != GetTestImage(runtime).Id {
|
||||
t.Errorf("Retrieved image Id differs, expected %s, received %s", GetTestImage(runtime).Id, images2[0].Id)
|
||||
found = false
|
||||
for _, img := range images2 {
|
||||
if img.ID == GetTestImage(runtime).ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Retrieved image Id differs, expected %s, received %+v", GetTestImage(runtime).ID, images2)
|
||||
}
|
||||
|
||||
r3 := httptest.NewRecorder()
|
||||
|
||||
// filter=a
|
||||
req3, err := http.NewRequest("GET", "/images/json?filter=a", nil)
|
||||
req3, err := http.NewRequest("GET", "/images/json?filter=aaaaaaaaaa", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := getImagesJson(srv, API_VERSION, r3, req3, nil); err != nil {
|
||||
if err := getImagesJSON(srv, APIVERSION, r3, req3, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images3 := []ApiImages{}
|
||||
images3 := []APIImages{}
|
||||
if err := json.Unmarshal(r3.Body.Bytes(), &images3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images3) != 0 {
|
||||
t.Errorf("Excepted 1 image, %d found", len(images3))
|
||||
t.Errorf("Excepted 0 image, %d found", len(images3))
|
||||
}
|
||||
|
||||
r4 := httptest.NewRecorder()
|
||||
@@ -199,7 +240,7 @@ func TestGetImagesJson(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = getImagesJson(srv, API_VERSION, r4, req4, nil)
|
||||
err = getImagesJSON(srv, APIVERSION, r4, req4, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("Error expected, received none")
|
||||
}
|
||||
@@ -220,7 +261,7 @@ func TestGetImagesViz(t *testing.T) {
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getImagesViz(srv, API_VERSION, r, nil, nil); err != nil {
|
||||
if err := getImagesViz(srv, APIVERSION, r, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -256,11 +297,11 @@ func TestGetImagesSearch(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := getImagesSearch(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := getImagesSearch(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
results := []ApiSearch{}
|
||||
results := []APISearch{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &results); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -280,11 +321,11 @@ func TestGetImagesHistory(t *testing.T) {
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
if err := getImagesHistory(srv, API_VERSION, r, nil, map[string]string{"name": unitTestImageName}); err != nil {
|
||||
if err := getImagesHistory(srv, APIVERSION, r, nil, map[string]string{"name": unitTestImageName}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
history := []ApiHistory{}
|
||||
history := []APIHistory{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &history); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -303,7 +344,7 @@ func TestGetImagesByName(t *testing.T) {
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getImagesByName(srv, API_VERSION, r, nil, map[string]string{"name": unitTestImageName}); err != nil {
|
||||
if err := getImagesByName(srv, APIVERSION, r, nil, map[string]string{"name": unitTestImageName}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -311,12 +352,12 @@ func TestGetImagesByName(t *testing.T) {
|
||||
if err := json.Unmarshal(r.Body.Bytes(), img); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if img.Id != GetTestImage(runtime).Id || img.Comment != "Imported from http://get.docker.io/images/busybox" {
|
||||
if img.ID != GetTestImage(runtime).ID || img.Comment != "Imported from http://get.docker.io/images/busybox" {
|
||||
t.Errorf("Error inspecting image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetContainersJson(t *testing.T) {
|
||||
func TestGetContainersJSON(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -326,7 +367,7 @@ func TestGetContainersJson(t *testing.T) {
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "test"},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -340,18 +381,18 @@ func TestGetContainersJson(t *testing.T) {
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getContainersJson(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := getContainersJSON(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
containers := []ApiContainers{}
|
||||
containers := []APIContainers{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &containers); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(containers) != 1 {
|
||||
t.Fatalf("Excepted %d container, %d found", 1, len(containers))
|
||||
}
|
||||
if containers[0].Id != container.Id {
|
||||
t.Fatalf("Container ID mismatch. Expected: %s, received: %s\n", container.Id, containers[0].Id)
|
||||
if containers[0].ID != container.ID {
|
||||
t.Fatalf("Container ID mismatch. Expected: %s, received: %s\n", container.ID, containers[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +410,7 @@ func TestGetContainersExport(t *testing.T) {
|
||||
// Create a container and remove a file
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"touch", "/test"},
|
||||
},
|
||||
)
|
||||
@@ -383,7 +424,7 @@ func TestGetContainersExport(t *testing.T) {
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err = getContainersExport(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err = getContainersExport(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -424,7 +465,7 @@ func TestGetContainersChanges(t *testing.T) {
|
||||
// Create a container and remove a file
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/rm", "/etc/passwd"},
|
||||
},
|
||||
)
|
||||
@@ -438,7 +479,7 @@ func TestGetContainersChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getContainersChanges(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := getContainersChanges(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changes := []Change{}
|
||||
@@ -472,7 +513,7 @@ func TestGetContainersByName(t *testing.T) {
|
||||
// Create a container and remove a file
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "test"},
|
||||
},
|
||||
)
|
||||
@@ -482,49 +523,15 @@ func TestGetContainersByName(t *testing.T) {
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getContainersByName(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := getContainersByName(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outContainer := &Container{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), outContainer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if outContainer.Id != container.Id {
|
||||
t.Fatalf("Wrong containers retrieved. Expected %s, recieved %s", container.Id, outContainer.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAuth(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
}
|
||||
|
||||
config := &auth.AuthConfig{
|
||||
Username: "utest",
|
||||
Email: "utest@yopmail.com",
|
||||
}
|
||||
|
||||
authStr := auth.EncodeAuth(config)
|
||||
auth.SaveConfig(runtime.root, authStr, config.Email)
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getAuth(srv, API_VERSION, r, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authConfig := &auth.AuthConfig{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), authConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if authConfig.Username != config.Username || authConfig.Email != config.Email {
|
||||
t.Errorf("The retrieve auth mismatch with the one set.")
|
||||
if outContainer.ID != container.ID {
|
||||
t.Fatalf("Wrong containers retrieved. Expected %s, recieved %s", container.ID, outContainer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +549,7 @@ func TestPostCommit(t *testing.T) {
|
||||
// Create a container and remove a file
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"touch", "/test"},
|
||||
},
|
||||
)
|
||||
@@ -555,24 +562,24 @@ func TestPostCommit(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/commit?repo=testrepo&testtag=tag&container="+container.Id, bytes.NewReader([]byte{}))
|
||||
req, err := http.NewRequest("POST", "/commit?repo=testrepo&testtag=tag&container="+container.ID, bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := postCommit(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := postCommit(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusCreated {
|
||||
t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code)
|
||||
}
|
||||
|
||||
apiId := &ApiId{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), apiId); err != nil {
|
||||
apiID := &APIID{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), apiID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := runtime.graph.Get(apiId.Id); err != nil {
|
||||
if _, err := runtime.graph.Get(apiID.ID); err != nil {
|
||||
t.Fatalf("The image has not been commited")
|
||||
}
|
||||
}
|
||||
@@ -715,7 +722,7 @@ func TestPostImagesInsert(t *testing.T) {
|
||||
// t.Fatalf("The test file has not been found")
|
||||
// }
|
||||
|
||||
// if err := srv.runtime.graph.Delete(img.Id); err != nil {
|
||||
// if err := srv.runtime.graph.Delete(img.ID); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
}
|
||||
@@ -824,8 +831,8 @@ func TestPostContainersCreate(t *testing.T) {
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
configJson, err := json.Marshal(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
configJSON, err := json.Marshal(&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Memory: 33554432,
|
||||
Cmd: []string{"touch", "/test"},
|
||||
})
|
||||
@@ -833,25 +840,25 @@ func TestPostContainersCreate(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/containers/create", bytes.NewReader(configJson))
|
||||
req, err := http.NewRequest("POST", "/containers/create", bytes.NewReader(configJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersCreate(srv, API_VERSION, r, req, nil); err != nil {
|
||||
if err := postContainersCreate(srv, APIVERSION, r, req, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusCreated {
|
||||
t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code)
|
||||
}
|
||||
|
||||
apiRun := &ApiRun{}
|
||||
apiRun := &APIRun{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), apiRun); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
container := srv.runtime.Get(apiRun.Id)
|
||||
container := srv.runtime.Get(apiRun.ID)
|
||||
if container == nil {
|
||||
t.Fatalf("Container not created")
|
||||
}
|
||||
@@ -880,7 +887,7 @@ func TestPostContainersKill(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -890,7 +897,8 @@ func TestPostContainersKill(t *testing.T) {
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -902,7 +910,7 @@ func TestPostContainersKill(t *testing.T) {
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersKill(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersKill(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusNoContent {
|
||||
@@ -924,7 +932,7 @@ func TestPostContainersRestart(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -934,7 +942,8 @@ func TestPostContainersRestart(t *testing.T) {
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -945,12 +954,12 @@ func TestPostContainersRestart(t *testing.T) {
|
||||
t.Errorf("Container should be running")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.Id+"/restart?t=1", bytes.NewReader([]byte{}))
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.ID+"/restart?t=1", bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersRestart(srv, API_VERSION, r, req, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersRestart(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusNoContent {
|
||||
@@ -980,7 +989,7 @@ func TestPostContainersStart(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -990,8 +999,15 @@ func TestPostContainersStart(t *testing.T) {
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
hostConfigJSON, err := json.Marshal(&HostConfig{})
|
||||
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.ID+"/start", bytes.NewReader(hostConfigJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersStart(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersStart(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusNoContent {
|
||||
@@ -1006,7 +1022,7 @@ func TestPostContainersStart(t *testing.T) {
|
||||
}
|
||||
|
||||
r = httptest.NewRecorder()
|
||||
if err = postContainersStart(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err == nil {
|
||||
if err = postContainersStart(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err == nil {
|
||||
t.Fatalf("A running containter should be able to be started")
|
||||
}
|
||||
|
||||
@@ -1026,7 +1042,7 @@ func TestPostContainersStop(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -1036,7 +1052,8 @@ func TestPostContainersStop(t *testing.T) {
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1048,12 +1065,12 @@ func TestPostContainersStop(t *testing.T) {
|
||||
}
|
||||
|
||||
// Note: as it is a POST request, it requires a body.
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.Id+"/stop?t=1", bytes.NewReader([]byte{}))
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.ID+"/stop?t=1", bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersStop(srv, API_VERSION, r, req, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersStop(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusNoContent {
|
||||
@@ -1075,7 +1092,7 @@ func TestPostContainersWait(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sleep", "1"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -1085,16 +1102,17 @@ func TestPostContainersWait(t *testing.T) {
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
setTimeout(t, "Wait timed out", 3*time.Second, func() {
|
||||
r := httptest.NewRecorder()
|
||||
if err := postContainersWait(srv, API_VERSION, r, nil, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersWait(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
apiWait := &ApiWait{}
|
||||
apiWait := &APIWait{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), apiWait); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1119,7 +1137,7 @@ func TestPostContainersAttach(t *testing.T) {
|
||||
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
OpenStdin: true,
|
||||
},
|
||||
@@ -1130,7 +1148,8 @@ func TestPostContainersAttach(t *testing.T) {
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
// Start the process
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1148,12 +1167,12 @@ func TestPostContainersAttach(t *testing.T) {
|
||||
out: stdoutPipe,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.Id+"/attach?stream=1&stdin=1&stdout=1&stderr=1", bytes.NewReader([]byte{}))
|
||||
req, err := http.NewRequest("POST", "/containers/"+container.ID+"/attach?stream=1&stdin=1&stdout=1&stderr=1", bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := postContainersAttach(srv, API_VERSION, r, req, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := postContainersAttach(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
@@ -1206,7 +1225,7 @@ func TestDeleteContainers(t *testing.T) {
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"touch", "/test"},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1218,19 +1237,19 @@ func TestDeleteContainers(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/containers/"+container.Id, nil)
|
||||
req, err := http.NewRequest("DELETE", "/containers/"+container.ID, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := httptest.NewRecorder()
|
||||
if err := deleteContainers(srv, API_VERSION, r, req, map[string]string{"name": container.Id}); err != nil {
|
||||
if err := deleteContainers(srv, APIVERSION, r, req, map[string]string{"name": container.ID}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusNoContent {
|
||||
t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code)
|
||||
}
|
||||
|
||||
if c := runtime.Get(container.Id); c != nil {
|
||||
if c := runtime.Get(container.ID); c != nil {
|
||||
t.Fatalf("The container as not been deleted")
|
||||
}
|
||||
|
||||
@@ -1239,9 +1258,136 @@ func TestDeleteContainers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsRoute(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime, enableCors: true}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
router, err := createRouter(srv, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("OPTIONS", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router.ServeHTTP(r, req)
|
||||
if r.Code != http.StatusOK {
|
||||
t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnabledCors(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime, enableCors: true}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
router, err := createRouter(srv, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/version", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router.ServeHTTP(r, req)
|
||||
if r.Code != http.StatusOK {
|
||||
t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code)
|
||||
}
|
||||
|
||||
allowOrigin := r.Header().Get("Access-Control-Allow-Origin")
|
||||
allowHeaders := r.Header().Get("Access-Control-Allow-Headers")
|
||||
allowMethods := r.Header().Get("Access-Control-Allow-Methods")
|
||||
|
||||
if allowOrigin != "*" {
|
||||
t.Errorf("Expected header Access-Control-Allow-Origin to be \"*\", %s found.", allowOrigin)
|
||||
}
|
||||
if allowHeaders != "Origin, X-Requested-With, Content-Type, Accept" {
|
||||
t.Errorf("Expected header Access-Control-Allow-Headers to be \"Origin, X-Requested-With, Content-Type, Accept\", %s found.", allowHeaders)
|
||||
}
|
||||
if allowMethods != "GET, POST, DELETE, PUT, OPTIONS" {
|
||||
t.Errorf("Expected hearder Access-Control-Allow-Methods to be \"GET, POST, DELETE, PUT, OPTIONS\", %s found.", allowMethods)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteImages(t *testing.T) {
|
||||
//FIXME: Implement this test
|
||||
t.Log("Test not implemented")
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
initialImages, err := srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := srv.runtime.repositories.Set("test", "test", unitTestImageName, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images, err := srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != len(initialImages)+1 {
|
||||
t.Errorf("Excepted %d images, %d found", len(initialImages)+1, len(images))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/images/test:test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := deleteImages(srv, APIVERSION, r, req, map[string]string{"name": "test:test"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusOK {
|
||||
t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
|
||||
}
|
||||
|
||||
var outs []APIRmi
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &outs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(outs) != 1 {
|
||||
t.Fatalf("Expected %d event (untagged), got %d", 1, len(outs))
|
||||
}
|
||||
images, err = srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != len(initialImages) {
|
||||
t.Errorf("Excepted %d image, %d found", len(initialImages), len(images))
|
||||
}
|
||||
|
||||
/* if c := runtime.Get(container.Id); c != nil {
|
||||
t.Fatalf("The container as not been deleted")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path.Join(container.rwPath(), "test")); err == nil {
|
||||
t.Fatalf("The test file has not been deleted")
|
||||
} */
|
||||
}
|
||||
|
||||
// Mocked types for tests
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Archive io.Reader
|
||||
@@ -20,6 +26,37 @@ const (
|
||||
Xz
|
||||
)
|
||||
|
||||
func DetectCompression(source []byte) Compression {
|
||||
for _, c := range source[:10] {
|
||||
utils.Debugf("%x", c)
|
||||
}
|
||||
|
||||
sourceLen := len(source)
|
||||
for compression, m := range map[Compression][]byte{
|
||||
Bzip2: {0x42, 0x5A, 0x68},
|
||||
Gzip: {0x1F, 0x8B, 0x08},
|
||||
Xz: {0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00},
|
||||
} {
|
||||
fail := false
|
||||
if len(m) > sourceLen {
|
||||
utils.Debugf("Len too short")
|
||||
continue
|
||||
}
|
||||
i := 0
|
||||
for _, b := range m {
|
||||
if b != source[i] {
|
||||
fail = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if !fail {
|
||||
return compression
|
||||
}
|
||||
}
|
||||
return Uncompressed
|
||||
}
|
||||
|
||||
func (compression *Compression) Flag() string {
|
||||
switch *compression {
|
||||
case Bzip2:
|
||||
@@ -46,14 +83,45 @@ func (compression *Compression) Extension() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Tar creates an archive from the directory at `path`, and returns it as a
|
||||
// stream of bytes.
|
||||
func Tar(path string, compression Compression) (io.Reader, error) {
|
||||
cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-c"+compression.Flag(), ".")
|
||||
return CmdStream(cmd)
|
||||
return TarFilter(path, compression, nil)
|
||||
}
|
||||
|
||||
// Tar creates an archive from the directory at `path`, only including files whose relative
|
||||
// paths are included in `filter`. If `filter` is nil, then all files are included.
|
||||
func TarFilter(path string, compression Compression, filter []string) (io.Reader, error) {
|
||||
args := []string{"tar", "--numeric-owner", "-f", "-", "-C", path}
|
||||
if filter == nil {
|
||||
filter = []string{"."}
|
||||
}
|
||||
for _, f := range filter {
|
||||
args = append(args, "-c"+compression.Flag(), f)
|
||||
}
|
||||
return CmdStream(exec.Command(args[0], args[1:]...))
|
||||
}
|
||||
|
||||
// Untar reads a stream of bytes from `archive`, parses it as a tar archive,
|
||||
// and unpacks it into the directory at `path`.
|
||||
// The archive may be compressed with one of the following algorithgms:
|
||||
// identity (uncompressed), gzip, bzip2, xz.
|
||||
// FIXME: specify behavior when target path exists vs. doesn't exist.
|
||||
func Untar(archive io.Reader, path string) error {
|
||||
cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x")
|
||||
cmd.Stdin = archive
|
||||
if archive == nil {
|
||||
return fmt.Errorf("Empty archive")
|
||||
}
|
||||
bufferedArchive := bufio.NewReaderSize(archive, 10)
|
||||
buf, err := bufferedArchive.Peek(10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compression := DetectCompression(buf)
|
||||
|
||||
utils.Debugf("Archive compression detected: %s", compression.Extension())
|
||||
|
||||
cmd := exec.Command("tar", "--numeric-owner", "-f", "-", "-C", path, "-x"+compression.Flag())
|
||||
cmd.Stdin = bufferedArchive
|
||||
// Hardcode locale environment for predictable outcome regardless of host configuration.
|
||||
// (see https://github.com/dotcloud/docker/issues/355)
|
||||
cmd.Env = []string{"LANG=en_US.utf-8", "LC_ALL=en_US.utf-8"}
|
||||
@@ -64,6 +132,95 @@ func Untar(archive io.Reader, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TarUntar is a convenience function which calls Tar and Untar, with
|
||||
// the output of one piped into the other. If either Tar or Untar fails,
|
||||
// TarUntar aborts and returns the error.
|
||||
func TarUntar(src string, filter []string, dst string) error {
|
||||
utils.Debugf("TarUntar(%s %s %s)", src, filter, dst)
|
||||
archive, err := TarFilter(src, Uncompressed, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Untar(archive, dst)
|
||||
}
|
||||
|
||||
// UntarPath is a convenience function which looks for an archive
|
||||
// at filesystem path `src`, and unpacks it at `dst`.
|
||||
func UntarPath(src, dst string) error {
|
||||
if archive, err := os.Open(src); err != nil {
|
||||
return err
|
||||
} else if err := Untar(archive, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyWithTar creates a tar archive of filesystem path `src`, and
|
||||
// unpacks it at filesystem path `dst`.
|
||||
// The archive is streamed directly with fixed buffering and no
|
||||
// intermediary disk IO.
|
||||
//
|
||||
func CopyWithTar(src, dst string) error {
|
||||
srcSt, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !srcSt.IsDir() {
|
||||
return CopyFileWithTar(src, dst)
|
||||
}
|
||||
// Create dst, copy src's content into it
|
||||
utils.Debugf("Creating dest directory: %s", dst)
|
||||
if err := os.MkdirAll(dst, 0700); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
utils.Debugf("Calling TarUntar(%s, %s)", src, dst)
|
||||
return TarUntar(src, nil, dst)
|
||||
}
|
||||
|
||||
// CopyFileWithTar emulates the behavior of the 'cp' command-line
|
||||
// for a single file. It copies a regular file from path `src` to
|
||||
// path `dst`, and preserves all its metadata.
|
||||
//
|
||||
// If `dst` ends with a trailing slash '/', the final destination path
|
||||
// will be `dst/base(src)`.
|
||||
func CopyFileWithTar(src, dst string) error {
|
||||
utils.Debugf("CopyFileWithTar(%s, %s)", src, dst)
|
||||
srcSt, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if srcSt.IsDir() {
|
||||
return fmt.Errorf("Can't copy a directory")
|
||||
}
|
||||
// Clean up the trailing /
|
||||
if dst[len(dst)-1] == '/' {
|
||||
dst = path.Join(dst, filepath.Base(src))
|
||||
}
|
||||
// Create the holding directory if necessary
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
hdr, err := tar.FileInfoHeader(srcSt, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hdr.Name = filepath.Base(dst)
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(tw, srcF); err != nil {
|
||||
return err
|
||||
}
|
||||
tw.Close()
|
||||
return Untar(buf, filepath.Dir(dst))
|
||||
}
|
||||
|
||||
// CmdStream executes a command, and returns its stdout as a stream.
|
||||
// If the command fails to run or doesn't complete successfully, an error
|
||||
// will be returned, including anything written on stderr.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -58,20 +61,58 @@ func TestCmdStreamGood(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTarUntar(t *testing.T) {
|
||||
archive, err := Tar(".", Uncompressed)
|
||||
func tarUntar(t *testing.T, origin string, compression Compression) error {
|
||||
archive, err := Tar(origin, compression)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 10)
|
||||
if _, err := archive.Read(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
archive = io.MultiReader(bytes.NewReader(buf), archive)
|
||||
|
||||
detectedCompression := DetectCompression(buf)
|
||||
if detectedCompression.Extension() != compression.Extension() {
|
||||
return fmt.Errorf("Wrong compression detected. Actual compression: %s, found %s", compression.Extension(), detectedCompression.Extension())
|
||||
}
|
||||
|
||||
tmp, err := ioutil.TempDir("", "docker-test-untar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
if err := Untar(archive, tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(tmp); err != nil {
|
||||
t.Fatalf("Error stating %s: %s", tmp, err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTarUntar(t *testing.T) {
|
||||
origin, err := ioutil.TempDir("", "docker-test-untar-origin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(origin)
|
||||
if err := ioutil.WriteFile(path.Join(origin, "1"), []byte("hello world"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(origin, "2"), []byte("welcome!"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, c := range []Compression{
|
||||
Uncompressed,
|
||||
Gzip,
|
||||
Bzip2,
|
||||
Xz,
|
||||
} {
|
||||
if err := tarUntar(t, origin, c); err != nil {
|
||||
t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
const CONFIGFILE = ".dockercfg"
|
||||
|
||||
// the registry server we want to login against
|
||||
const INDEX_SERVER = "https://index.docker.io/v1"
|
||||
const INDEXSERVER = "https://index.docker.io/v1"
|
||||
|
||||
//const INDEX_SERVER = "http://indexstaging-docker.dotcloud.com/"
|
||||
//const INDEXSERVER = "http://indexstaging-docker.dotcloud.com/"
|
||||
|
||||
var (
|
||||
ErrConfigFileMissing error = errors.New("The Auth config file is missing")
|
||||
ErrConfigFileMissing = errors.New("The Auth config file is missing")
|
||||
)
|
||||
|
||||
type AuthConfig struct {
|
||||
@@ -44,11 +44,11 @@ func IndexServerAddress() string {
|
||||
if os.Getenv("DOCKER_INDEX_URL") != "" {
|
||||
return os.Getenv("DOCKER_INDEX_URL") + "/v1"
|
||||
}
|
||||
return INDEX_SERVER
|
||||
return INDEXSERVER
|
||||
}
|
||||
|
||||
// create a base64 encoded auth string to store in config
|
||||
func EncodeAuth(authConfig *AuthConfig) string {
|
||||
func encodeAuth(authConfig *AuthConfig) string {
|
||||
authStr := authConfig.Username + ":" + authConfig.Password
|
||||
msg := []byte(authStr)
|
||||
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
|
||||
@@ -57,7 +57,7 @@ func EncodeAuth(authConfig *AuthConfig) string {
|
||||
}
|
||||
|
||||
// decode the auth string
|
||||
func DecodeAuth(authStr string) (*AuthConfig, error) {
|
||||
func decodeAuth(authStr string) (*AuthConfig, error) {
|
||||
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
||||
decoded := make([]byte, decLen)
|
||||
authByte := []byte(authStr)
|
||||
@@ -74,7 +74,6 @@ func DecodeAuth(authStr string) (*AuthConfig, error) {
|
||||
}
|
||||
password := strings.Trim(arr[1], "\x00")
|
||||
return &AuthConfig{Username: arr[0], Password: password}, nil
|
||||
|
||||
}
|
||||
|
||||
// load up the auth config information and return values
|
||||
@@ -82,7 +81,7 @@ func DecodeAuth(authStr string) (*AuthConfig, error) {
|
||||
func LoadConfig(rootPath string) (*AuthConfig, error) {
|
||||
confFile := path.Join(rootPath, CONFIGFILE)
|
||||
if _, err := os.Stat(confFile); err != nil {
|
||||
return nil, ErrConfigFileMissing
|
||||
return &AuthConfig{rootPath: rootPath}, ErrConfigFileMissing
|
||||
}
|
||||
b, err := ioutil.ReadFile(confFile)
|
||||
if err != nil {
|
||||
@@ -94,7 +93,7 @@ func LoadConfig(rootPath string) (*AuthConfig, error) {
|
||||
}
|
||||
origAuth := strings.Split(arr[0], " = ")
|
||||
origEmail := strings.Split(arr[1], " = ")
|
||||
authConfig, err := DecodeAuth(origAuth[1])
|
||||
authConfig, err := decodeAuth(origAuth[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,13 +103,13 @@ func LoadConfig(rootPath string) (*AuthConfig, error) {
|
||||
}
|
||||
|
||||
// save the auth config
|
||||
func SaveConfig(rootPath, authStr string, email string) error {
|
||||
confFile := path.Join(rootPath, CONFIGFILE)
|
||||
if len(email) == 0 {
|
||||
func SaveConfig(authConfig *AuthConfig) error {
|
||||
confFile := path.Join(authConfig.rootPath, CONFIGFILE)
|
||||
if len(authConfig.Email) == 0 {
|
||||
os.Remove(confFile)
|
||||
return nil
|
||||
}
|
||||
lines := "auth = " + authStr + "\n" + "email = " + email + "\n"
|
||||
lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n"
|
||||
b := []byte(lines)
|
||||
err := ioutil.WriteFile(confFile, b, 0600)
|
||||
if err != nil {
|
||||
@@ -120,7 +119,7 @@ func SaveConfig(rootPath, authStr string, email string) error {
|
||||
}
|
||||
|
||||
// try to register/login to the registry server
|
||||
func Login(authConfig *AuthConfig) (string, error) {
|
||||
func Login(authConfig *AuthConfig, store bool) (string, error) {
|
||||
storeConfig := false
|
||||
client := &http.Client{}
|
||||
reqStatusCode := 0
|
||||
@@ -146,7 +145,7 @@ func Login(authConfig *AuthConfig) (string, error) {
|
||||
|
||||
if reqStatusCode == 201 {
|
||||
status = "Account created. Please use the confirmation link we sent" +
|
||||
" to your e-mail to activate it.\n"
|
||||
" to your e-mail to activate it."
|
||||
storeConfig = true
|
||||
} else if reqStatusCode == 403 {
|
||||
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
|
||||
@@ -165,11 +164,14 @@ func Login(authConfig *AuthConfig) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode == 200 {
|
||||
status = "Login Succeeded\n"
|
||||
status = "Login Succeeded"
|
||||
storeConfig = true
|
||||
} else if resp.StatusCode == 401 {
|
||||
if err := SaveConfig(authConfig.rootPath, "", ""); err != nil {
|
||||
return "", err
|
||||
if store {
|
||||
authConfig.Email = ""
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Wrong login/password, please try again")
|
||||
} else {
|
||||
@@ -182,9 +184,8 @@ func Login(authConfig *AuthConfig) (string, error) {
|
||||
} else {
|
||||
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
|
||||
}
|
||||
if storeConfig {
|
||||
authStr := EncodeAuth(authConfig)
|
||||
if err := SaveConfig(authConfig.rootPath, authStr, authConfig.Email); err != nil {
|
||||
if storeConfig && store {
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
func TestEncodeAuth(t *testing.T) {
|
||||
newAuthConfig := &AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"}
|
||||
authStr := EncodeAuth(newAuthConfig)
|
||||
decAuthConfig, err := DecodeAuth(authStr)
|
||||
authStr := encodeAuth(newAuthConfig)
|
||||
decAuthConfig, err := decodeAuth(authStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -30,11 +30,11 @@ func TestLogin(t *testing.T) {
|
||||
os.Setenv("DOCKER_INDEX_URL", "https://indexstaging-docker.dotcloud.com")
|
||||
defer os.Setenv("DOCKER_INDEX_URL", "")
|
||||
authConfig := NewAuthConfig("unittester", "surlautrerivejetattendrai", "noise+unittester@dotcloud.com", "/tmp")
|
||||
status, err := Login(authConfig)
|
||||
status, err := Login(authConfig, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != "Login Succeeded\n" {
|
||||
if status != "Login Succeeded" {
|
||||
t.Fatalf("Expected status \"Login Succeeded\", found \"%s\" instead", status)
|
||||
}
|
||||
}
|
||||
@@ -50,17 +50,17 @@ func TestCreateAccount(t *testing.T) {
|
||||
token := hex.EncodeToString(tokenBuffer)[:12]
|
||||
username := "ut" + token
|
||||
authConfig := NewAuthConfig(username, "test42", "docker-ut+"+token+"@example.com", "/tmp")
|
||||
status, err := Login(authConfig)
|
||||
status, err := Login(authConfig, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedStatus := "Account created. Please use the confirmation link we sent" +
|
||||
" to your e-mail to activate it.\n"
|
||||
" to your e-mail to activate it."
|
||||
if status != expectedStatus {
|
||||
t.Fatalf("Expected status: \"%s\", found \"%s\" instead.", expectedStatus, status)
|
||||
}
|
||||
|
||||
status, err = Login(authConfig)
|
||||
status, err = Login(authConfig, false)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but found nil instead")
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
var defaultDns = []string{"8.8.8.8", "8.8.4.4"}
|
||||
|
||||
type Builder struct {
|
||||
runtime *Runtime
|
||||
repositories *TagStore
|
||||
@@ -40,41 +43,63 @@ func (builder *Builder) Create(config *Config) (*Container, error) {
|
||||
}
|
||||
|
||||
// Generate id
|
||||
id := GenerateId()
|
||||
id := GenerateID()
|
||||
// Generate default hostname
|
||||
// FIXME: the lxc template no longer needs to set a default hostname
|
||||
if config.Hostname == "" {
|
||||
config.Hostname = id[:12]
|
||||
}
|
||||
|
||||
var args []string
|
||||
var entrypoint string
|
||||
|
||||
if len(config.Entrypoint) != 0 {
|
||||
entrypoint = config.Entrypoint[0]
|
||||
args = append(config.Entrypoint[1:], config.Cmd...)
|
||||
} else {
|
||||
entrypoint = config.Cmd[0]
|
||||
args = config.Cmd[1:]
|
||||
}
|
||||
|
||||
container := &Container{
|
||||
// FIXME: we should generate the ID here instead of receiving it as an argument
|
||||
Id: id,
|
||||
ID: id,
|
||||
Created: time.Now(),
|
||||
Path: config.Cmd[0],
|
||||
Args: config.Cmd[1:], //FIXME: de-duplicate from config
|
||||
Path: entrypoint,
|
||||
Args: args, //FIXME: de-duplicate from config
|
||||
Config: config,
|
||||
Image: img.Id, // Always use the resolved image id
|
||||
Image: img.ID, // Always use the resolved image id
|
||||
NetworkSettings: &NetworkSettings{},
|
||||
// FIXME: do we need to store this in the container?
|
||||
SysInitPath: sysInitPath,
|
||||
}
|
||||
container.root = builder.runtime.containerRoot(container.Id)
|
||||
container.root = builder.runtime.containerRoot(container.ID)
|
||||
// Step 1: create the container directory.
|
||||
// This doubles as a barrier to avoid race conditions.
|
||||
if err := os.Mkdir(container.root, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(config.Dns) == 0 && len(builder.runtime.Dns) == 0 && utils.CheckLocalDns() {
|
||||
//"WARNING: Docker detected local DNS server on resolv.conf. Using default external servers: %v", defaultDns
|
||||
builder.runtime.Dns = defaultDns
|
||||
}
|
||||
|
||||
// If custom dns exists, then create a resolv.conf for the container
|
||||
if len(config.Dns) > 0 {
|
||||
if len(config.Dns) > 0 || len(builder.runtime.Dns) > 0 {
|
||||
var dns []string
|
||||
if len(config.Dns) > 0 {
|
||||
dns = config.Dns
|
||||
} else {
|
||||
dns = builder.runtime.Dns
|
||||
}
|
||||
container.ResolvConfPath = path.Join(container.root, "resolv.conf")
|
||||
f, err := os.Create(container.ResolvConfPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
for _, dns := range config.Dns {
|
||||
for _, dns := range dns {
|
||||
if _, err := f.Write([]byte("nameserver " + dns + "\n")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,7 +135,7 @@ func (builder *Builder) Commit(container *Container, repository, tag, comment, a
|
||||
}
|
||||
// Register the image if needed
|
||||
if repository != "" {
|
||||
if err := builder.repositories.Set(repository, tag, img.Id, true); err != nil {
|
||||
if err := builder.repositories.Set(repository, tag, img.ID, true); err != nil {
|
||||
return img, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type builderClient struct {
|
||||
cli *DockerCli
|
||||
|
||||
image string
|
||||
maintainer string
|
||||
config *Config
|
||||
|
||||
tmpContainers map[string]struct{}
|
||||
tmpImages map[string]struct{}
|
||||
|
||||
needCommit bool
|
||||
}
|
||||
|
||||
func (b *builderClient) clearTmp(containers, images map[string]struct{}) {
|
||||
for i := range images {
|
||||
if _, _, err := b.cli.call("DELETE", "/images/"+i, nil); err != nil {
|
||||
utils.Debugf("%s", err)
|
||||
}
|
||||
utils.Debugf("Removing image %s", i)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdFrom(name string) error {
|
||||
obj, statusCode, err := b.cli.call("GET", "/images/"+name+"/json", nil)
|
||||
if statusCode == 404 {
|
||||
|
||||
remote := name
|
||||
var tag string
|
||||
if strings.Contains(remote, ":") {
|
||||
remoteParts := strings.Split(remote, ":")
|
||||
tag = remoteParts[1]
|
||||
remote = remoteParts[0]
|
||||
}
|
||||
var out io.Writer
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
out = os.Stdout
|
||||
} else {
|
||||
out = &utils.NopWriter{}
|
||||
}
|
||||
if err := b.cli.stream("POST", "/images/create?fromImage="+remote+"&tag="+tag, nil, out); err != nil {
|
||||
return err
|
||||
}
|
||||
obj, _, err = b.cli.call("GET", "/images/"+name+"/json", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
img := &ApiId{}
|
||||
if err := json.Unmarshal(obj, img); err != nil {
|
||||
return err
|
||||
}
|
||||
b.image = img.Id
|
||||
utils.Debugf("Using image %s", b.image)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdMaintainer(name string) error {
|
||||
b.needCommit = true
|
||||
b.maintainer = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdRun(args string) error {
|
||||
if b.image == "" {
|
||||
return fmt.Errorf("Please provide a source image with `from` prior to run")
|
||||
}
|
||||
config, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, env := b.config.Cmd, b.config.Env
|
||||
b.config.Cmd = nil
|
||||
MergeConfig(b.config, config)
|
||||
|
||||
body, statusCode, err := b.cli.call("POST", "/images/getCache", &ApiImageConfig{Id: b.image, Config: b.config})
|
||||
if err != nil {
|
||||
if statusCode != 404 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if statusCode != 404 {
|
||||
apiId := &ApiId{}
|
||||
if err := json.Unmarshal(body, apiId); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Debugf("Use cached version")
|
||||
b.image = apiId.Id
|
||||
return nil
|
||||
}
|
||||
cid, err := b.run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.Cmd, b.config.Env = cmd, env
|
||||
return b.commit(cid)
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdEnv(args string) error {
|
||||
b.needCommit = true
|
||||
tmp := strings.SplitN(args, " ", 2)
|
||||
if len(tmp) != 2 {
|
||||
return fmt.Errorf("Invalid ENV format")
|
||||
}
|
||||
key := strings.Trim(tmp[0], " ")
|
||||
value := strings.Trim(tmp[1], " ")
|
||||
|
||||
for i, elem := range b.config.Env {
|
||||
if strings.HasPrefix(elem, key+"=") {
|
||||
b.config.Env[i] = key + "=" + value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
b.config.Env = append(b.config.Env, key+"="+value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdCmd(args string) error {
|
||||
b.needCommit = true
|
||||
var cmd []string
|
||||
if err := json.Unmarshal([]byte(args), &cmd); err != nil {
|
||||
utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err)
|
||||
b.config.Cmd = []string{"/bin/sh", "-c", args}
|
||||
} else {
|
||||
b.config.Cmd = cmd
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdExpose(args string) error {
|
||||
ports := strings.Split(args, " ")
|
||||
b.config.PortSpecs = append(ports, b.config.PortSpecs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) CmdInsert(args string) error {
|
||||
// tmp := strings.SplitN(args, "\t ", 2)
|
||||
// sourceUrl, destPath := tmp[0], tmp[1]
|
||||
|
||||
// v := url.Values{}
|
||||
// v.Set("url", sourceUrl)
|
||||
// v.Set("path", destPath)
|
||||
// body, _, err := b.cli.call("POST", "/images/insert?"+v.Encode(), nil)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// apiId := &ApiId{}
|
||||
// if err := json.Unmarshal(body, apiId); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// FIXME: Reimplement this, we need to retrieve the resulting Id
|
||||
return fmt.Errorf("INSERT not implemented")
|
||||
}
|
||||
|
||||
func (b *builderClient) run() (string, error) {
|
||||
if b.image == "" {
|
||||
return "", fmt.Errorf("Please provide a source image with `from` prior to run")
|
||||
}
|
||||
b.config.Image = b.image
|
||||
body, _, err := b.cli.call("POST", "/containers/create", b.config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
apiRun := &ApiRun{}
|
||||
if err := json.Unmarshal(body, apiRun); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, warning := range apiRun.Warnings {
|
||||
fmt.Fprintln(os.Stderr, "WARNING: ", warning)
|
||||
}
|
||||
|
||||
//start the container
|
||||
_, _, err = b.cli.call("POST", "/containers/"+apiRun.Id+"/start", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.tmpContainers[apiRun.Id] = struct{}{}
|
||||
|
||||
// Wait for it to finish
|
||||
body, _, err = b.cli.call("POST", "/containers/"+apiRun.Id+"/wait", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
apiWait := &ApiWait{}
|
||||
if err := json.Unmarshal(body, apiWait); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if apiWait.StatusCode != 0 {
|
||||
return "", fmt.Errorf("The command %v returned a non-zero code: %d", b.config.Cmd, apiWait.StatusCode)
|
||||
}
|
||||
|
||||
return apiRun.Id, nil
|
||||
}
|
||||
|
||||
func (b *builderClient) commit(id string) error {
|
||||
if b.image == "" {
|
||||
return fmt.Errorf("Please provide a source image with `from` prior to run")
|
||||
}
|
||||
b.config.Image = b.image
|
||||
|
||||
if id == "" {
|
||||
cmd := b.config.Cmd
|
||||
b.config.Cmd = []string{"true"}
|
||||
if cid, err := b.run(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
id = cid
|
||||
}
|
||||
b.config.Cmd = cmd
|
||||
}
|
||||
|
||||
// Commit the container
|
||||
v := url.Values{}
|
||||
v.Set("container", id)
|
||||
v.Set("author", b.maintainer)
|
||||
|
||||
body, _, err := b.cli.call("POST", "/commit?"+v.Encode(), b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiId := &ApiId{}
|
||||
if err := json.Unmarshal(body, apiId); err != nil {
|
||||
return err
|
||||
}
|
||||
b.tmpImages[apiId.Id] = struct{}{}
|
||||
b.image = apiId.Id
|
||||
b.needCommit = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *builderClient) Build(dockerfile, context io.Reader) (string, error) {
|
||||
defer b.clearTmp(b.tmpContainers, b.tmpImages)
|
||||
file := bufio.NewReader(dockerfile)
|
||||
for {
|
||||
line, err := file.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
line = strings.Replace(strings.TrimSpace(line), " ", " ", 1)
|
||||
// Skip comments and empty line
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
tmp := strings.SplitN(line, " ", 2)
|
||||
if len(tmp) != 2 {
|
||||
return "", fmt.Errorf("Invalid Dockerfile format")
|
||||
}
|
||||
instruction := strings.ToLower(strings.Trim(tmp[0], " "))
|
||||
arguments := strings.Trim(tmp[1], " ")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s %s (%s)\n", strings.ToUpper(instruction), arguments, b.image)
|
||||
|
||||
method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
|
||||
if !exists {
|
||||
fmt.Fprintf(os.Stderr, "Skipping unknown instruction %s\n", strings.ToUpper(instruction))
|
||||
}
|
||||
ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
|
||||
if ret != nil {
|
||||
return "", ret.(error)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "===> %v\n", b.image)
|
||||
}
|
||||
if b.needCommit {
|
||||
if err := b.commit(""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if b.image != "" {
|
||||
// The build is successful, keep the temporary containers and images
|
||||
for i := range b.tmpImages {
|
||||
delete(b.tmpImages, i)
|
||||
}
|
||||
for i := range b.tmpContainers {
|
||||
delete(b.tmpContainers, i)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Build finished. image id: %s\n", b.image)
|
||||
return b.image, nil
|
||||
}
|
||||
return "", fmt.Errorf("An error occured during the build\n")
|
||||
}
|
||||
|
||||
func NewBuilderClient(addr string, port int) BuildFile {
|
||||
return &builderClient{
|
||||
cli: NewDockerCli(addr, port),
|
||||
config: &Config{},
|
||||
tmpContainers: make(map[string]struct{}),
|
||||
tmpImages: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
type BuildFile interface {
|
||||
Build(io.Reader, io.Reader) (string, error)
|
||||
Build(io.Reader) (string, error)
|
||||
CmdFrom(string) error
|
||||
CmdRun(string) error
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (b *buildFile) CmdFrom(name string) error {
|
||||
remote = name
|
||||
}
|
||||
|
||||
if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false)); err != nil {
|
||||
if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (b *buildFile) CmdFrom(name string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.image = image.Id
|
||||
b.image = image.ID
|
||||
b.config = &Config{}
|
||||
return nil
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (b *buildFile) CmdRun(args string) error {
|
||||
if b.image == "" {
|
||||
return fmt.Errorf("Please provide a source image with `from` prior to run")
|
||||
}
|
||||
config, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil)
|
||||
config, _, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,8 +101,9 @@ func (b *buildFile) CmdRun(args string) error {
|
||||
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
|
||||
return err
|
||||
} else if cache != nil {
|
||||
fmt.Fprintf(b.out, " ---> Using cache\n")
|
||||
utils.Debugf("[BUILDER] Use cached version")
|
||||
b.image = cache.Id
|
||||
b.image = cache.ID
|
||||
return nil
|
||||
} else {
|
||||
utils.Debugf("[BUILDER] Cache miss")
|
||||
@@ -124,8 +125,8 @@ func (b *buildFile) CmdEnv(args string) error {
|
||||
if len(tmp) != 2 {
|
||||
return fmt.Errorf("Invalid ENV format")
|
||||
}
|
||||
key := strings.Trim(tmp[0], " ")
|
||||
value := strings.Trim(tmp[1], " ")
|
||||
key := strings.Trim(tmp[0], " \t")
|
||||
value := strings.Trim(tmp[1], " \t")
|
||||
|
||||
for i, elem := range b.config.Env {
|
||||
if strings.HasPrefix(elem, key+"=") {
|
||||
@@ -140,7 +141,7 @@ func (b *buildFile) CmdEnv(args string) error {
|
||||
func (b *buildFile) CmdCmd(args string) error {
|
||||
var cmd []string
|
||||
if err := json.Unmarshal([]byte(args), &cmd); err != nil {
|
||||
utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err)
|
||||
utils.Debugf("Error unmarshalling: %s, setting cmd to /bin/sh -c", err)
|
||||
cmd = []string{"/bin/sh", "-c", args}
|
||||
}
|
||||
if err := b.commit("", cmd, fmt.Sprintf("CMD %v", cmd)); err != nil {
|
||||
@@ -164,6 +165,62 @@ func (b *buildFile) CmdCopy(args string) error {
|
||||
return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
|
||||
}
|
||||
|
||||
func (b *buildFile) CmdEntrypoint(args string) error {
|
||||
if args == "" {
|
||||
return fmt.Errorf("Entrypoint cannot be empty")
|
||||
}
|
||||
|
||||
var entrypoint []string
|
||||
if err := json.Unmarshal([]byte(args), &entrypoint); err != nil {
|
||||
b.config.Entrypoint = []string{"/bin/sh", "-c", args}
|
||||
} else {
|
||||
b.config.Entrypoint = entrypoint
|
||||
}
|
||||
if err := b.commit("", b.config.Cmd, fmt.Sprintf("ENTRYPOINT %s", args)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *buildFile) addRemote(container *Container, orig, dest string) error {
|
||||
file, err := utils.Download(orig, ioutil.Discard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Body.Close()
|
||||
|
||||
return container.Inject(file.Body, dest)
|
||||
}
|
||||
|
||||
func (b *buildFile) addContext(container *Container, orig, dest string) error {
|
||||
origPath := path.Join(b.context, orig)
|
||||
destPath := path.Join(container.RootfsPath(), dest)
|
||||
// Preserve the trailing '/'
|
||||
if dest[len(dest)-1] == '/' {
|
||||
destPath = destPath + "/"
|
||||
}
|
||||
fi, err := os.Stat(origPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if err := CopyWithTar(origPath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// First try to unpack the source as an archive
|
||||
} else if err := UntarPath(origPath, destPath); err != nil {
|
||||
utils.Debugf("Couldn't untar %s to %s: %s", origPath, destPath, err)
|
||||
// If that fails, just copy it as a regular file
|
||||
if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := CopyWithTar(origPath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *buildFile) CmdAdd(args string) error {
|
||||
if b.context == "" {
|
||||
return fmt.Errorf("No context given. Impossible to use ADD")
|
||||
@@ -172,55 +229,36 @@ func (b *buildFile) CmdAdd(args string) error {
|
||||
if len(tmp) != 2 {
|
||||
return fmt.Errorf("Invalid ADD format")
|
||||
}
|
||||
orig := strings.Trim(tmp[0], " ")
|
||||
dest := strings.Trim(tmp[1], " ")
|
||||
orig := strings.Trim(tmp[0], " \t")
|
||||
dest := strings.Trim(tmp[1], " \t")
|
||||
|
||||
cmd := b.config.Cmd
|
||||
b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
|
||||
cid, err := b.run()
|
||||
|
||||
b.config.Image = b.image
|
||||
// Create the container and start it
|
||||
container, err := b.builder.Create(b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.tmpContainers[container.ID] = struct{}{}
|
||||
|
||||
container := b.runtime.Get(cid)
|
||||
if container == nil {
|
||||
return fmt.Errorf("Error while creating the container (CmdAdd)")
|
||||
}
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer container.Unmount()
|
||||
|
||||
origPath := path.Join(b.context, orig)
|
||||
destPath := path.Join(container.RootfsPath(), dest)
|
||||
|
||||
fi, err := os.Stat(origPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if err := os.MkdirAll(destPath, 0700); err != nil {
|
||||
if utils.IsURL(orig) {
|
||||
if err := b.addRemote(container, orig, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(path.Join(b.context, orig))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fi := range files {
|
||||
if err := utils.CopyDirectory(path.Join(origPath, fi.Name()), path.Join(destPath, fi.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := os.MkdirAll(path.Dir(destPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.CopyDirectory(origPath, destPath); err != nil {
|
||||
if err := b.addContext(container, orig, dest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := b.commit(cid, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
|
||||
|
||||
if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.Cmd = cmd
|
||||
@@ -238,10 +276,12 @@ func (b *buildFile) run() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.tmpContainers[c.Id] = struct{}{}
|
||||
b.tmpContainers[c.ID] = struct{}{}
|
||||
fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(c.ID))
|
||||
|
||||
//start the container
|
||||
if err := c.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := c.Start(hostConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -250,7 +290,7 @@ func (b *buildFile) run() (string, error) {
|
||||
return "", fmt.Errorf("The command %v returned a non-zero code: %d", b.config.Cmd, ret)
|
||||
}
|
||||
|
||||
return c.Id, nil
|
||||
return c.ID, nil
|
||||
}
|
||||
|
||||
// Commit the container <id> with the autorun command <autoCmd>
|
||||
@@ -260,23 +300,31 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
|
||||
}
|
||||
b.config.Image = b.image
|
||||
if id == "" {
|
||||
cmd := b.config.Cmd
|
||||
b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
|
||||
defer func(cmd []string) { b.config.Cmd = cmd }(cmd)
|
||||
|
||||
if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
|
||||
return err
|
||||
} else if cache != nil {
|
||||
fmt.Fprintf(b.out, " ---> Using cache\n")
|
||||
utils.Debugf("[BUILDER] Use cached version")
|
||||
b.image = cache.Id
|
||||
b.image = cache.ID
|
||||
return nil
|
||||
} else {
|
||||
utils.Debugf("[BUILDER] Cache miss")
|
||||
}
|
||||
|
||||
if cid, err := b.run(); err != nil {
|
||||
container, err := b.builder.Create(b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
id = cid
|
||||
}
|
||||
b.tmpContainers[container.ID] = struct{}{}
|
||||
fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(container.ID))
|
||||
id = container.ID
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer container.Unmount()
|
||||
}
|
||||
|
||||
container := b.runtime.Get(id)
|
||||
@@ -292,33 +340,40 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.tmpImages[image.Id] = struct{}{}
|
||||
b.image = image.Id
|
||||
b.tmpImages[image.ID] = struct{}{}
|
||||
b.image = image.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
|
||||
if context != nil {
|
||||
name, err := ioutil.TempDir("/tmp", "docker-build")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := Untar(context, name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(name)
|
||||
b.context = name
|
||||
func (b *buildFile) Build(context io.Reader) (string, error) {
|
||||
// FIXME: @creack any reason for using /tmp instead of ""?
|
||||
// FIXME: @creack "name" is a terrible variable name
|
||||
name, err := ioutil.TempDir("/tmp", "docker-build")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := Untar(context, name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(name)
|
||||
b.context = name
|
||||
dockerfile, err := os.Open(path.Join(name, "Dockerfile"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Can't build a directory with no Dockerfile")
|
||||
}
|
||||
// FIXME: "file" is also a terrible variable name ;)
|
||||
file := bufio.NewReader(dockerfile)
|
||||
stepN := 0
|
||||
for {
|
||||
line, err := file.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if err == io.EOF && line == "" {
|
||||
break
|
||||
} else if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
line = strings.Replace(strings.TrimSpace(line), " ", " ", 1)
|
||||
line = strings.Trim(strings.Replace(line, "\t", " ", -1), " \t\r\n")
|
||||
// Skip comments and empty line
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
@@ -329,12 +384,13 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
|
||||
}
|
||||
instruction := strings.ToLower(strings.Trim(tmp[0], " "))
|
||||
arguments := strings.Trim(tmp[1], " ")
|
||||
|
||||
fmt.Fprintf(b.out, "%s %s (%s)\n", strings.ToUpper(instruction), arguments, b.image)
|
||||
stepN += 1
|
||||
// FIXME: only count known instructions as build steps
|
||||
fmt.Fprintf(b.out, "Step %d : %s %s\n", stepN, strings.ToUpper(instruction), arguments)
|
||||
|
||||
method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
|
||||
if !exists {
|
||||
fmt.Fprintf(b.out, "Skipping unknown instruction %s\n", strings.ToUpper(instruction))
|
||||
fmt.Fprintf(b.out, "# Skipping unknown instruction %s\n", strings.ToUpper(instruction))
|
||||
continue
|
||||
}
|
||||
ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
|
||||
@@ -342,10 +398,10 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
|
||||
return "", ret.(error)
|
||||
}
|
||||
|
||||
fmt.Fprintf(b.out, "===> %v\n", b.image)
|
||||
fmt.Fprintf(b.out, " ---> %v\n", utils.TruncateID(b.image))
|
||||
}
|
||||
if b.image != "" {
|
||||
fmt.Fprintf(b.out, "Build successful.\n===> %s\n", b.image)
|
||||
fmt.Fprintf(b.out, "Successfully built %s\n", utils.TruncateID(b.image))
|
||||
return b.image, nil
|
||||
}
|
||||
return "", fmt.Errorf("An error occured during the build\n")
|
||||
|
||||
@@ -1,72 +1,118 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
"testing"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const Dockerfile = `
|
||||
# VERSION 0.1
|
||||
# DOCKER-VERSION 0.2
|
||||
// mkTestContext generates a build context from the contents of the provided dockerfile.
|
||||
// This context is suitable for use as an argument to BuildFile.Build()
|
||||
func mkTestContext(dockerfile string, files [][2]string, t *testing.T) Archive {
|
||||
context, err := mkBuildContext(fmt.Sprintf(dockerfile, unitTestImageId), files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
from ` + unitTestImageName + `
|
||||
// A testContextTemplate describes a build context and how to test it
|
||||
type testContextTemplate struct {
|
||||
// Contents of the Dockerfile
|
||||
dockerfile string
|
||||
// Additional files in the context, eg [][2]string{"./passwd", "gordon"}
|
||||
files [][2]string
|
||||
}
|
||||
|
||||
// A table of all the contexts to build and test.
|
||||
// A new docker runtime will be created and torn down for each context.
|
||||
var testContexts []testContextTemplate = []testContextTemplate{
|
||||
{
|
||||
`
|
||||
from %s
|
||||
run sh -c 'echo root:testpass > /tmp/passwd'
|
||||
run mkdir -p /var/run/sshd
|
||||
`
|
||||
run [ "$(cat /tmp/passwd)" = "root:testpass" ]
|
||||
run [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]
|
||||
`,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
add foo /usr/lib/bla/bar
|
||||
run [ "$(cat /usr/lib/bla/bar)" = 'hello world!' ]
|
||||
`,
|
||||
[][2]string{{"foo", "hello world!"}},
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
add f /
|
||||
run [ "$(cat /f)" = "hello" ]
|
||||
add f /abc
|
||||
run [ "$(cat /abc)" = "hello" ]
|
||||
add f /x/y/z
|
||||
run [ "$(cat /x/y/z)" = "hello" ]
|
||||
add f /x/y/d/
|
||||
run [ "$(cat /x/y/d/f)" = "hello" ]
|
||||
add d /
|
||||
run [ "$(cat /ga)" = "bu" ]
|
||||
add d /somewhere
|
||||
run [ "$(cat /somewhere/ga)" = "bu" ]
|
||||
add d /anotherplace/
|
||||
run [ "$(cat /anotherplace/ga)" = "bu" ]
|
||||
add d /somewheeeere/over/the/rainbooow
|
||||
run [ "$(cat /somewheeeere/over/the/rainbooow/ga)" = "bu" ]
|
||||
`,
|
||||
[][2]string{
|
||||
{"f", "hello"},
|
||||
{"d/ga", "bu"},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from %s
|
||||
env FOO BAR
|
||||
run [ "$FOO" = "BAR" ]
|
||||
`,
|
||||
nil,
|
||||
},
|
||||
|
||||
{
|
||||
`
|
||||
from docker-ut
|
||||
ENTRYPOINT /bin/echo
|
||||
CMD Hello world
|
||||
`,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
// FIXME: test building with 2 successive overlapping ADD commands
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
for _, ctx := range testContexts {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
lock: &sync.Mutex{},
|
||||
pullingPool: make(map[string]struct{}),
|
||||
pushingPool: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
buildfile := NewBuildFile(srv, &utils.NopWriter{})
|
||||
|
||||
imgId, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgId,
|
||||
Cmd: []string{"cat", "/tmp/passwd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
output, err := container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "root:testpass\n" {
|
||||
t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n")
|
||||
}
|
||||
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgId,
|
||||
Cmd: []string{"ls", "-d", "/var/run/sshd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container2)
|
||||
|
||||
output, err = container2.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "/var/run/sshd\n" {
|
||||
t.Fatal("/var/run/sshd has not been created")
|
||||
buildfile := NewBuildFile(srv, ioutil.Discard)
|
||||
if _, err := buildfile.Build(mkTestContext(ctx.dockerfile, ctx.files, t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package docker
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
_ "io/ioutil"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -59,20 +60,6 @@ func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error
|
||||
}
|
||||
|
||||
/*TODO
|
||||
func cmdWait(srv *Server, container *Container) error {
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
go func() {
|
||||
srv.CmdWait(nil, stdoutPipe, container.Id)
|
||||
}()
|
||||
|
||||
if _, err := bufio.NewReader(stdout).ReadString('\n'); err != nil {
|
||||
return err
|
||||
}
|
||||
// Cleanup pipes
|
||||
return closeWrap(stdout, stdoutPipe)
|
||||
}
|
||||
|
||||
func cmdImages(srv *Server, args ...string) (string, error) {
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
@@ -144,41 +131,39 @@ func TestImages(t *testing.T) {
|
||||
// todo: add checks for -a
|
||||
}
|
||||
|
||||
*/
|
||||
// TestRunHostname checks that 'docker run -h' correctly sets a custom hostname
|
||||
func TestRunHostname(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
stdin, _ := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(nil, stdoutPipe, nil, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
if err := srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-h", "foobar", GetTestImage(runtime).Id, "hostname"); err != nil {
|
||||
defer close(c)
|
||||
if err := cli.CmdRun("-h", "foobar", unitTestImageId, "hostname"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
close(c)
|
||||
}()
|
||||
cmdOutput, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cmdOutput != "foobar\n" {
|
||||
t.Fatalf("'hostname' should display '%s', not '%s'", "foobar\n", cmdOutput)
|
||||
}
|
||||
utils.Debugf("--")
|
||||
setTimeout(t, "Reading command output time out", 2*time.Second, func() {
|
||||
cmdOutput, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cmdOutput != "foobar\n" {
|
||||
t.Fatalf("'hostname' should display '%s', not '%s'", "foobar\n", cmdOutput)
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(t, "CmdRun timed out", 2*time.Second, func() {
|
||||
setTimeout(t, "CmdRun timed out", 5*time.Second, func() {
|
||||
<-c
|
||||
cmdWait(srv, srv.runtime.List()[0])
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
func TestRunExit(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
@@ -334,29 +319,27 @@ func TestRunDisconnectTty(t *testing.T) {
|
||||
t.Fatalf("/bin/cat should still be running after closing stdin (tty mode)")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// TestAttachStdin checks attaching to stdin without stdout and stderr.
|
||||
// 'docker run -i -a stdin' should sends the client's stdin to the command,
|
||||
// then detach from it and print the container id.
|
||||
func TestRunAttachStdin(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
stdin, stdinPipe := io.Pipe()
|
||||
stdout, stdoutPipe := io.Pipe()
|
||||
|
||||
cli := NewDockerCli(stdin, stdoutPipe, nil, testDaemonProto, testDaemonAddr)
|
||||
defer cleanup(globalRuntime)
|
||||
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-i", "-a", "stdin", GetTestImage(runtime).Id, "sh", "-c", "echo hello; cat")
|
||||
close(ch)
|
||||
defer close(ch)
|
||||
cli.CmdRun("-i", "-a", "stdin", unitTestImageId, "sh", "-c", "echo hello && cat")
|
||||
}()
|
||||
|
||||
// Send input to the command, close stdin
|
||||
setTimeout(t, "Write timed out", 2*time.Second, func() {
|
||||
setTimeout(t, "Write timed out", 10*time.Second, func() {
|
||||
if _, err := stdinPipe.Write([]byte("hi there\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -365,23 +348,27 @@ func TestRunAttachStdin(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
container := runtime.List()[0]
|
||||
container := globalRuntime.List()[0]
|
||||
|
||||
// Check output
|
||||
cmdOutput, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cmdOutput != container.ShortId()+"\n" {
|
||||
t.Fatalf("Wrong output: should be '%s', not '%s'\n", container.ShortId()+"\n", cmdOutput)
|
||||
}
|
||||
setTimeout(t, "Reading command output time out", 10*time.Second, func() {
|
||||
cmdOutput, err := bufio.NewReader(stdout).ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cmdOutput != container.ShortID()+"\n" {
|
||||
t.Fatalf("Wrong output: should be '%s', not '%s'\n", container.ShortID()+"\n", cmdOutput)
|
||||
}
|
||||
})
|
||||
|
||||
// wait for CmdRun to return
|
||||
setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() {
|
||||
setTimeout(t, "Waiting for CmdRun timed out", 5*time.Second, func() {
|
||||
// Unblock hijack end
|
||||
stdout.Read([]byte{})
|
||||
<-ch
|
||||
})
|
||||
|
||||
setTimeout(t, "Waiting for command to exit timed out", 2*time.Second, func() {
|
||||
setTimeout(t, "Waiting for command to exit timed out", 5*time.Second, func() {
|
||||
container.Wait()
|
||||
})
|
||||
|
||||
@@ -400,6 +387,7 @@ func TestRunAttachStdin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Expected behaviour, the process stays alive when the client disconnects
|
||||
func TestAttachDisconnect(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
type Container struct {
|
||||
root string
|
||||
|
||||
Id string
|
||||
ID string
|
||||
|
||||
Created time.Time
|
||||
|
||||
@@ -51,6 +52,9 @@ type Container struct {
|
||||
|
||||
waitLock chan struct{}
|
||||
Volumes map[string]string
|
||||
// Store rw/ro in a separate structure to preserve reserve-compatibility on-disk.
|
||||
// Easier than migrating older container configs :)
|
||||
VolumesRW map[string]bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -72,10 +76,21 @@ type Config struct {
|
||||
Image string // Name of the image as it was passed by the operator (eg. could be symbolic)
|
||||
Volumes map[string]struct{}
|
||||
VolumesFrom string
|
||||
Entrypoint []string
|
||||
}
|
||||
|
||||
func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet, error) {
|
||||
cmd := Subcmd("run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container")
|
||||
type HostConfig struct {
|
||||
Binds []string
|
||||
}
|
||||
|
||||
type BindMap struct {
|
||||
SrcPath string
|
||||
DstPath string
|
||||
Mode string
|
||||
}
|
||||
|
||||
func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, *flag.FlagSet, error) {
|
||||
cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
|
||||
if len(args) > 0 && args[0] != "--help" {
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
}
|
||||
@@ -109,12 +124,16 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet
|
||||
cmd.Var(flVolumes, "v", "Attach a data volume")
|
||||
|
||||
flVolumesFrom := cmd.String("volumes-from", "", "Mount volumes from the specified container")
|
||||
flEntrypoint := cmd.String("entrypoint", "", "Overwrite the default entrypoint of the image")
|
||||
|
||||
var flBinds ListOpts
|
||||
cmd.Var(&flBinds, "b", "Bind mount a volume from the host (e.g. -b /host:/container)")
|
||||
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return nil, cmd, err
|
||||
return nil, nil, cmd, err
|
||||
}
|
||||
if *flDetach && len(flAttach) > 0 {
|
||||
return nil, cmd, fmt.Errorf("Conflicting options: -a and -d")
|
||||
return nil, nil, cmd, fmt.Errorf("Conflicting options: -a and -d")
|
||||
}
|
||||
// If neither -d or -a are set, attach to everything by default
|
||||
if len(flAttach) == 0 && !*flDetach {
|
||||
@@ -126,8 +145,17 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add any bind targets to the list of container volumes
|
||||
for _, bind := range flBinds {
|
||||
arr := strings.Split(bind, ":")
|
||||
dstDir := arr[1]
|
||||
flVolumes[dstDir] = struct{}{}
|
||||
}
|
||||
|
||||
parsedArgs := cmd.Args()
|
||||
runCmd := []string{}
|
||||
entrypoint := []string{}
|
||||
image := ""
|
||||
if len(parsedArgs) >= 1 {
|
||||
image = cmd.Arg(0)
|
||||
@@ -135,6 +163,10 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet
|
||||
if len(parsedArgs) > 1 {
|
||||
runCmd = parsedArgs[1:]
|
||||
}
|
||||
if *flEntrypoint != "" {
|
||||
entrypoint = []string{*flEntrypoint}
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Hostname: *flHostname,
|
||||
PortSpecs: flPorts,
|
||||
@@ -152,6 +184,10 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet
|
||||
Image: image,
|
||||
Volumes: flVolumes,
|
||||
VolumesFrom: *flVolumesFrom,
|
||||
Entrypoint: entrypoint,
|
||||
}
|
||||
hostConfig := &HostConfig{
|
||||
Binds: flBinds,
|
||||
}
|
||||
|
||||
if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit {
|
||||
@@ -163,12 +199,12 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet
|
||||
if config.OpenStdin && config.AttachStdin {
|
||||
config.StdinOnce = true
|
||||
}
|
||||
return config, cmd, nil
|
||||
return config, hostConfig, cmd, nil
|
||||
}
|
||||
|
||||
type NetworkSettings struct {
|
||||
IpAddress string
|
||||
IpPrefixLen int
|
||||
IPAddress string
|
||||
IPPrefixLen int
|
||||
Gateway string
|
||||
Bridge string
|
||||
PortMapping map[string]string
|
||||
@@ -355,6 +391,18 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
||||
errors <- err
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if stdinCloser != nil {
|
||||
defer stdinCloser.Close()
|
||||
}
|
||||
|
||||
if cStdout, err := container.StdoutPipe(); err != nil {
|
||||
utils.Debugf("Error stdout pipe")
|
||||
} else {
|
||||
io.Copy(&utils.NopWriter{}, cStdout)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if stderr != nil {
|
||||
nJobs += 1
|
||||
@@ -381,7 +429,20 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
||||
errors <- err
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if stdinCloser != nil {
|
||||
defer stdinCloser.Close()
|
||||
}
|
||||
|
||||
if cStderr, err := container.StderrPipe(); err != nil {
|
||||
utils.Debugf("Error stdout pipe")
|
||||
} else {
|
||||
io.Copy(&utils.NopWriter{}, cStderr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return utils.Go(func() error {
|
||||
if cStdout != nil {
|
||||
defer cStdout.Close()
|
||||
@@ -404,12 +465,12 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
||||
})
|
||||
}
|
||||
|
||||
func (container *Container) Start() error {
|
||||
func (container *Container) Start(hostConfig *HostConfig) error {
|
||||
container.State.lock()
|
||||
defer container.State.unlock()
|
||||
|
||||
if container.State.Running {
|
||||
return fmt.Errorf("The container %s is already running.", container.Id)
|
||||
return fmt.Errorf("The container %s is already running.", container.ID)
|
||||
}
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
@@ -428,27 +489,81 @@ func (container *Container) Start() error {
|
||||
container.Config.MemorySwap = -1
|
||||
}
|
||||
container.Volumes = make(map[string]string)
|
||||
container.VolumesRW = make(map[string]bool)
|
||||
|
||||
// Create the requested bind mounts
|
||||
binds := make(map[string]BindMap)
|
||||
// Define illegal container destinations
|
||||
illegal_dsts := []string{"/", "."}
|
||||
|
||||
for _, bind := range hostConfig.Binds {
|
||||
// FIXME: factorize bind parsing in parseBind
|
||||
var src, dst, mode string
|
||||
arr := strings.Split(bind, ":")
|
||||
if len(arr) == 2 {
|
||||
src = arr[0]
|
||||
dst = arr[1]
|
||||
mode = "rw"
|
||||
} else if len(arr) == 3 {
|
||||
src = arr[0]
|
||||
dst = arr[1]
|
||||
mode = arr[2]
|
||||
} else {
|
||||
return fmt.Errorf("Invalid bind specification: %s", bind)
|
||||
}
|
||||
|
||||
// Bail if trying to mount to an illegal destination
|
||||
for _, illegal := range illegal_dsts {
|
||||
if dst == illegal {
|
||||
return fmt.Errorf("Illegal bind destination: %s", dst)
|
||||
}
|
||||
}
|
||||
|
||||
bindMap := BindMap{
|
||||
SrcPath: src,
|
||||
DstPath: dst,
|
||||
Mode: mode,
|
||||
}
|
||||
binds[path.Clean(dst)] = bindMap
|
||||
}
|
||||
|
||||
// FIXME: evaluate volumes-from before individual volumes, so that the latter can override the former.
|
||||
// Create the requested volumes volumes
|
||||
for volPath := range container.Config.Volumes {
|
||||
if c, err := container.runtime.volumes.Create(nil, container, "", "", nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil {
|
||||
return nil
|
||||
volPath = path.Clean(volPath)
|
||||
// If an external bind is defined for this volume, use that as a source
|
||||
if bindMap, exists := binds[volPath]; exists {
|
||||
container.Volumes[volPath] = bindMap.SrcPath
|
||||
if strings.ToLower(bindMap.Mode) == "rw" {
|
||||
container.VolumesRW[volPath] = true
|
||||
}
|
||||
container.Volumes[volPath] = c.Id
|
||||
// Otherwise create an directory in $ROOT/volumes/ and use that
|
||||
} else {
|
||||
c, err := container.runtime.volumes.Create(nil, container, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcPath, err := c.layer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
container.Volumes[volPath] = srcPath
|
||||
container.VolumesRW[volPath] = true // RW by default
|
||||
}
|
||||
// Create the mountpoint
|
||||
if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if container.Config.VolumesFrom != "" {
|
||||
c := container.runtime.Get(container.Config.VolumesFrom)
|
||||
if c == nil {
|
||||
return fmt.Errorf("Container %s not found. Impossible to mount its volumes", container.Id)
|
||||
return fmt.Errorf("Container %s not found. Impossible to mount its volumes", container.ID)
|
||||
}
|
||||
for volPath, id := range c.Volumes {
|
||||
if _, exists := container.Volumes[volPath]; exists {
|
||||
return fmt.Errorf("The requested volume %s overlap one of the volume of the container %s", volPath, c.Id)
|
||||
return fmt.Errorf("The requested volume %s overlap one of the volume of the container %s", volPath, c.ID)
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil {
|
||||
return nil
|
||||
@@ -462,7 +577,7 @@ func (container *Container) Start() error {
|
||||
}
|
||||
|
||||
params := []string{
|
||||
"-n", container.Id,
|
||||
"-n", container.ID,
|
||||
"-f", container.lxcConfigPath(),
|
||||
"--",
|
||||
"/sbin/init",
|
||||
@@ -526,7 +641,8 @@ func (container *Container) Start() error {
|
||||
}
|
||||
|
||||
func (container *Container) Run() error {
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
container.Wait()
|
||||
@@ -539,7 +655,8 @@ func (container *Container) Output() (output []byte, err error) {
|
||||
return nil, err
|
||||
}
|
||||
defer pipe.Close()
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err = ioutil.ReadAll(pipe)
|
||||
@@ -573,17 +690,17 @@ func (container *Container) allocateNetwork() error {
|
||||
}
|
||||
container.NetworkSettings.PortMapping = make(map[string]string)
|
||||
for _, spec := range container.Config.PortSpecs {
|
||||
if nat, err := iface.AllocatePort(spec); err != nil {
|
||||
nat, err := iface.AllocatePort(spec)
|
||||
if err != nil {
|
||||
iface.Release()
|
||||
return err
|
||||
} else {
|
||||
container.NetworkSettings.PortMapping[strconv.Itoa(nat.Backend)] = strconv.Itoa(nat.Frontend)
|
||||
}
|
||||
container.NetworkSettings.PortMapping[strconv.Itoa(nat.Backend)] = strconv.Itoa(nat.Frontend)
|
||||
}
|
||||
container.network = iface
|
||||
container.NetworkSettings.Bridge = container.runtime.networkManager.bridgeIface
|
||||
container.NetworkSettings.IpAddress = iface.IPNet.IP.String()
|
||||
container.NetworkSettings.IpPrefixLen, _ = iface.IPNet.Mask.Size()
|
||||
container.NetworkSettings.IPAddress = iface.IPNet.IP.String()
|
||||
container.NetworkSettings.IPPrefixLen, _ = iface.IPNet.Mask.Size()
|
||||
container.NetworkSettings.Gateway = iface.Gateway.String()
|
||||
return nil
|
||||
}
|
||||
@@ -597,16 +714,15 @@ func (container *Container) releaseNetwork() {
|
||||
// FIXME: replace this with a control socket within docker-init
|
||||
func (container *Container) waitLxc() error {
|
||||
for {
|
||||
if output, err := exec.Command("lxc-info", "-n", container.Id).CombinedOutput(); err != nil {
|
||||
output, err := exec.Command("lxc-info", "-n", container.ID).CombinedOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if !strings.Contains(string(output), "RUNNING") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(output), "RUNNING") {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (container *Container) monitor() {
|
||||
@@ -616,17 +732,17 @@ func (container *Container) monitor() {
|
||||
// If the command does not exists, try to wait via lxc
|
||||
if container.cmd == nil {
|
||||
if err := container.waitLxc(); err != nil {
|
||||
utils.Debugf("%s: Process: %s", container.Id, err)
|
||||
utils.Debugf("%s: Process: %s", container.ID, err)
|
||||
}
|
||||
} else {
|
||||
if err := container.cmd.Wait(); err != nil {
|
||||
// Discard the error as any signals or non 0 returns will generate an error
|
||||
utils.Debugf("%s: Process: %s", container.Id, err)
|
||||
utils.Debugf("%s: Process: %s", container.ID, err)
|
||||
}
|
||||
}
|
||||
utils.Debugf("Process finished")
|
||||
|
||||
var exitCode int = -1
|
||||
exitCode := -1
|
||||
if container.cmd != nil {
|
||||
exitCode = container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
|
||||
}
|
||||
@@ -635,24 +751,24 @@ func (container *Container) monitor() {
|
||||
container.releaseNetwork()
|
||||
if container.Config.OpenStdin {
|
||||
if err := container.stdin.Close(); err != nil {
|
||||
utils.Debugf("%s: Error close stdin: %s", container.Id, err)
|
||||
utils.Debugf("%s: Error close stdin: %s", container.ID, err)
|
||||
}
|
||||
}
|
||||
if err := container.stdout.CloseWriters(); err != nil {
|
||||
utils.Debugf("%s: Error close stdout: %s", container.Id, err)
|
||||
utils.Debugf("%s: Error close stdout: %s", container.ID, err)
|
||||
}
|
||||
if err := container.stderr.CloseWriters(); err != nil {
|
||||
utils.Debugf("%s: Error close stderr: %s", container.Id, err)
|
||||
utils.Debugf("%s: Error close stderr: %s", container.ID, err)
|
||||
}
|
||||
|
||||
if container.ptyMaster != nil {
|
||||
if err := container.ptyMaster.Close(); err != nil {
|
||||
utils.Debugf("%s: Error closing Pty master: %s", container.Id, err)
|
||||
utils.Debugf("%s: Error closing Pty master: %s", container.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := container.Unmount(); err != nil {
|
||||
log.Printf("%v: Failed to umount filesystem: %v", container.Id, err)
|
||||
log.Printf("%v: Failed to umount filesystem: %v", container.ID, err)
|
||||
}
|
||||
|
||||
// Re-create a brand new stdin pipe once the container exited
|
||||
@@ -673,7 +789,7 @@ func (container *Container) monitor() {
|
||||
// This is because State.setStopped() has already been called, and has caused Wait()
|
||||
// to return.
|
||||
// FIXME: why are we serializing running state to disk in the first place?
|
||||
//log.Printf("%s: Failed to dump configuration to the disk: %s", container.Id, err)
|
||||
//log.Printf("%s: Failed to dump configuration to the disk: %s", container.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,17 +799,17 @@ func (container *Container) kill() error {
|
||||
}
|
||||
|
||||
// Sending SIGKILL to the process via lxc
|
||||
output, err := exec.Command("lxc-kill", "-n", container.Id, "9").CombinedOutput()
|
||||
output, err := exec.Command("lxc-kill", "-n", container.ID, "9").CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("error killing container %s (%s, %s)", container.Id, output, err)
|
||||
log.Printf("error killing container %s (%s, %s)", container.ID, output, err)
|
||||
}
|
||||
|
||||
// 2. Wait for the process to die, in last resort, try to kill the process directly
|
||||
if err := container.WaitTimeout(10 * time.Second); err != nil {
|
||||
if container.cmd == nil {
|
||||
return fmt.Errorf("lxc-kill failed, impossible to kill the container %s", container.Id)
|
||||
return fmt.Errorf("lxc-kill failed, impossible to kill the container %s", container.ID)
|
||||
}
|
||||
log.Printf("Container %s failed to exit within 10 seconds of lxc SIGKILL - trying direct SIGKILL", container.Id)
|
||||
log.Printf("Container %s failed to exit within 10 seconds of lxc SIGKILL - trying direct SIGKILL", container.ID)
|
||||
if err := container.cmd.Process.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -721,7 +837,7 @@ func (container *Container) Stop(seconds int) error {
|
||||
}
|
||||
|
||||
// 1. Send a SIGTERM
|
||||
if output, err := exec.Command("lxc-kill", "-n", container.Id, "15").CombinedOutput(); err != nil {
|
||||
if output, err := exec.Command("lxc-kill", "-n", container.ID, "15").CombinedOutput(); err != nil {
|
||||
log.Print(string(output))
|
||||
log.Print("Failed to send SIGTERM to the process, force killing")
|
||||
if err := container.kill(); err != nil {
|
||||
@@ -731,7 +847,7 @@ func (container *Container) Stop(seconds int) error {
|
||||
|
||||
// 2. Wait for the process to exit on its own
|
||||
if err := container.WaitTimeout(time.Duration(seconds) * time.Second); err != nil {
|
||||
log.Printf("Container %v failed to exit within %d seconds of SIGTERM - using the force", container.Id, seconds)
|
||||
log.Printf("Container %v failed to exit within %d seconds of SIGTERM - using the force", container.ID, seconds)
|
||||
if err := container.kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -743,7 +859,8 @@ func (container *Container) Restart(seconds int) error {
|
||||
if err := container.Stop(seconds); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -795,7 +912,6 @@ func (container *Container) WaitTimeout(timeout time.Duration) error {
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (container *Container) EnsureMounted() error {
|
||||
@@ -838,16 +954,16 @@ func (container *Container) Unmount() error {
|
||||
return Unmount(container.RootfsPath())
|
||||
}
|
||||
|
||||
// ShortId returns a shorthand version of the container's id for convenience.
|
||||
// ShortID returns a shorthand version of the container's id for convenience.
|
||||
// A collision with other container shorthands is very unlikely, but possible.
|
||||
// In case of a collision a lookup with Runtime.Get() will fail, and the caller
|
||||
// will need to use a langer prefix, or the full-length container Id.
|
||||
func (container *Container) ShortId() string {
|
||||
return utils.TruncateId(container.Id)
|
||||
func (container *Container) ShortID() string {
|
||||
return utils.TruncateID(container.ID)
|
||||
}
|
||||
|
||||
func (container *Container) logPath(name string) string {
|
||||
return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name))
|
||||
return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.ID, name))
|
||||
}
|
||||
|
||||
func (container *Container) ReadLog(name string) (io.Reader, error) {
|
||||
@@ -867,29 +983,36 @@ func (container *Container) RootfsPath() string {
|
||||
return path.Join(container.root, "rootfs")
|
||||
}
|
||||
|
||||
func (container *Container) GetVolumes() (map[string]string, error) {
|
||||
ret := make(map[string]string)
|
||||
for volPath, id := range container.Volumes {
|
||||
volume, err := container.runtime.volumes.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := volume.root()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[volPath] = path.Join(root, "layer")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (container *Container) rwPath() string {
|
||||
return path.Join(container.root, "rw")
|
||||
}
|
||||
|
||||
func validateId(id string) error {
|
||||
func validateID(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("Invalid empty id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSize, return real size, virtual size
|
||||
func (container *Container) GetSize() (int64, int64) {
|
||||
var sizeRw, sizeRootfs int64
|
||||
|
||||
filepath.Walk(container.rwPath(), func(path string, fileInfo os.FileInfo, err error) error {
|
||||
if fileInfo != nil {
|
||||
sizeRw += fileInfo.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
_, err := os.Stat(container.RootfsPath())
|
||||
if err == nil {
|
||||
filepath.Walk(container.RootfsPath(), func(path string, fileInfo os.FileInfo, err error) error {
|
||||
if fileInfo != nil {
|
||||
sizeRootfs += fileInfo.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return sizeRw, sizeRootfs
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -14,39 +15,33 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIdFormat(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
func TestIDFormat(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container1, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c", "echo hello world"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
match, err := regexp.Match("^[0-9a-f]{64}$", []byte(container1.Id))
|
||||
match, err := regexp.Match("^[0-9a-f]{64}$", []byte(container1.ID))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("Invalid container ID: %s", container1.Id)
|
||||
t.Fatalf("Invalid container ID: %s", container1.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleAttachRestart(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c",
|
||||
"i=1; while [ $i -le 5 ]; do i=`expr $i + 1`; echo hello; done"},
|
||||
},
|
||||
@@ -70,7 +65,8 @@ func TestMultipleAttachRestart(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l1, err := bufio.NewReader(stdout1).ReadString('\n')
|
||||
@@ -111,7 +107,7 @@ func TestMultipleAttachRestart(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -142,10 +138,7 @@ func TestMultipleAttachRestart(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
@@ -153,7 +146,7 @@ func TestDiff(t *testing.T) {
|
||||
// Create a container and remove a file
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/rm", "/etc/passwd"},
|
||||
},
|
||||
)
|
||||
@@ -194,7 +187,7 @@ func TestDiff(t *testing.T) {
|
||||
// Create a new container from the commited image
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.Id,
|
||||
Image: img.ID,
|
||||
Cmd: []string{"cat", "/etc/passwd"},
|
||||
},
|
||||
)
|
||||
@@ -221,7 +214,7 @@ func TestDiff(t *testing.T) {
|
||||
// Create a new containere
|
||||
container3, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"rm", "/bin/httpd"},
|
||||
},
|
||||
)
|
||||
@@ -251,16 +244,13 @@ func TestDiff(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommitAutoRun(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c", "echo hello > /world"},
|
||||
},
|
||||
)
|
||||
@@ -291,7 +281,7 @@ func TestCommitAutoRun(t *testing.T) {
|
||||
// FIXME: Make a TestCommit that stops here and check docker.root/layers/img.id/world
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.Id,
|
||||
Image: img.ID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -306,7 +296,8 @@ func TestCommitAutoRun(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container2.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container2.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2.Wait()
|
||||
@@ -330,17 +321,14 @@ func TestCommitAutoRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommitRun(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
container1, err := builder.Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/sh", "-c", "echo hello > /world"},
|
||||
},
|
||||
)
|
||||
@@ -372,7 +360,7 @@ func TestCommitRun(t *testing.T) {
|
||||
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: img.Id,
|
||||
Image: img.ID,
|
||||
Cmd: []string{"cat", "/world"},
|
||||
},
|
||||
)
|
||||
@@ -388,7 +376,8 @@ func TestCommitRun(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container2.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container2.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container2.Wait()
|
||||
@@ -412,14 +401,11 @@ func TestCommitRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Memory: 33554432,
|
||||
CpuShares: 1000,
|
||||
Cmd: []string{"/bin/cat"},
|
||||
@@ -436,7 +422,8 @@ func TestStart(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -446,7 +433,7 @@ func TestStart(t *testing.T) {
|
||||
if !container.State.Running {
|
||||
t.Errorf("Container should be running")
|
||||
}
|
||||
if err := container.Start(); err == nil {
|
||||
if err := container.Start(hostConfig); err == nil {
|
||||
t.Fatalf("A running containter should be able to be started")
|
||||
}
|
||||
|
||||
@@ -456,14 +443,11 @@ func TestStart(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"ls", "-al"},
|
||||
},
|
||||
)
|
||||
@@ -484,14 +468,11 @@ func TestRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOutput(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "-n", "foobar"},
|
||||
},
|
||||
)
|
||||
@@ -509,13 +490,10 @@ func TestOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestKillDifferentUser(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"tail", "-f", "/etc/resolv.conf"},
|
||||
User: "daemon",
|
||||
},
|
||||
@@ -528,7 +506,8 @@ func TestKillDifferentUser(t *testing.T) {
|
||||
if container.State.Running {
|
||||
t.Errorf("Container shouldn't be running")
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -556,15 +535,33 @@ func TestKillDifferentUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKill(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
// Test that creating a container with a volume doesn't crash. Regression test for #995.
|
||||
func TestCreateVolume(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
config, hc, _, err := ParseRun([]string{"-v", "/var/lib/data", GetTestImage(runtime).ID, "echo", "hello", "world"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := NewBuilder(runtime).Create(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(c)
|
||||
if err := c.Start(hc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.WaitTimeout(500 * time.Millisecond)
|
||||
c.Wait()
|
||||
}
|
||||
|
||||
func TestKill(t *testing.T) {
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Cmd: []string{"cat", "/dev/zero"},
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"sleep", "2"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -575,7 +572,8 @@ func TestKill(t *testing.T) {
|
||||
if container.State.Running {
|
||||
t.Errorf("Container shouldn't be running")
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -602,16 +600,13 @@ func TestKill(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExitCode(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
trueContainer, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/true", ""},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -626,7 +621,7 @@ func TestExitCode(t *testing.T) {
|
||||
}
|
||||
|
||||
falseContainer, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/false", ""},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -642,13 +637,10 @@ func TestExitCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRestart(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "-n", "foobar"},
|
||||
},
|
||||
)
|
||||
@@ -675,13 +667,10 @@ func TestRestart(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRestartStdin(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"cat"},
|
||||
|
||||
OpenStdin: true,
|
||||
@@ -700,7 +689,8 @@ func TestRestartStdin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(stdin, "hello world"); err != nil {
|
||||
@@ -730,7 +720,7 @@ func TestRestartStdin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := io.WriteString(stdin, "hello world #2"); err != nil {
|
||||
@@ -753,17 +743,14 @@ func TestRestartStdin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
// Default user must be root
|
||||
container, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"id"},
|
||||
},
|
||||
)
|
||||
@@ -781,7 +768,7 @@ func TestUser(t *testing.T) {
|
||||
|
||||
// Set a username
|
||||
container, err = builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"id"},
|
||||
|
||||
User: "root",
|
||||
@@ -801,7 +788,7 @@ func TestUser(t *testing.T) {
|
||||
|
||||
// Set a UID
|
||||
container, err = builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"id"},
|
||||
|
||||
User: "0",
|
||||
@@ -821,7 +808,7 @@ func TestUser(t *testing.T) {
|
||||
|
||||
// Set a different user by uid
|
||||
container, err = builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"id"},
|
||||
|
||||
User: "1",
|
||||
@@ -843,7 +830,7 @@ func TestUser(t *testing.T) {
|
||||
|
||||
// Set a different user by username
|
||||
container, err = builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"id"},
|
||||
|
||||
User: "daemon",
|
||||
@@ -863,17 +850,14 @@ func TestUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMultipleContainers(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
|
||||
container1, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Cmd: []string{"cat", "/dev/zero"},
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"sleep", "2"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -882,8 +866,8 @@ func TestMultipleContainers(t *testing.T) {
|
||||
defer runtime.Destroy(container1)
|
||||
|
||||
container2, err := builder.Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Cmd: []string{"cat", "/dev/zero"},
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"sleep", "2"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -892,10 +876,11 @@ func TestMultipleContainers(t *testing.T) {
|
||||
defer runtime.Destroy(container2)
|
||||
|
||||
// Start both containers
|
||||
if err := container1.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container1.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container2.Start(); err != nil {
|
||||
if err := container2.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -922,13 +907,10 @@ func TestMultipleContainers(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStdin(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"cat"},
|
||||
|
||||
OpenStdin: true,
|
||||
@@ -947,7 +929,8 @@ func TestStdin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stdin.Close()
|
||||
@@ -969,13 +952,10 @@ func TestStdin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTty(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"cat"},
|
||||
|
||||
OpenStdin: true,
|
||||
@@ -994,7 +974,8 @@ func TestTty(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stdin.Close()
|
||||
@@ -1016,13 +997,10 @@ func TestTty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/usr/bin/env"},
|
||||
},
|
||||
)
|
||||
@@ -1036,7 +1014,8 @@ func TestEnv(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
container.Wait()
|
||||
@@ -1064,6 +1043,32 @@ func TestEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntrypoint(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
container, err := NewBuilder(runtime).Create(
|
||||
&Config{
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Entrypoint: []string{"/bin/echo"},
|
||||
Cmd: []string{"-n", "foobar"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
output, err := container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "foobar" {
|
||||
t.Error(string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func grepFile(t *testing.T, path string, pattern string) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -1085,10 +1090,7 @@ func grepFile(t *testing.T, path string, pattern string) {
|
||||
}
|
||||
|
||||
func TestLXCConfig(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runtime := mkRuntime(t)
|
||||
defer nuke(runtime)
|
||||
// Memory is allocated randomly for testing
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
@@ -1100,7 +1102,7 @@ func TestLXCConfig(t *testing.T) {
|
||||
cpuMax := 10000
|
||||
cpu := cpuMin + rand.Intn(cpuMax-cpuMin)
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"/bin/true"},
|
||||
|
||||
Hostname: "foobar",
|
||||
@@ -1128,7 +1130,7 @@ func BenchmarkRunSequencial(b *testing.B) {
|
||||
defer nuke(runtime)
|
||||
for i := 0; i < b.N; i++ {
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "-n", "foo"},
|
||||
},
|
||||
)
|
||||
@@ -1163,7 +1165,7 @@ func BenchmarkRunParallel(b *testing.B) {
|
||||
tasks = append(tasks, complete)
|
||||
go func(i int, complete chan error) {
|
||||
container, err := NewBuilder(runtime).Create(&Config{
|
||||
Image: GetTestImage(runtime).Id,
|
||||
Image: GetTestImage(runtime).ID,
|
||||
Cmd: []string{"echo", "-n", "foo"},
|
||||
},
|
||||
)
|
||||
@@ -1172,7 +1174,8 @@ func BenchmarkRunParallel(b *testing.B) {
|
||||
return
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
if err := container.Start(); err != nil {
|
||||
hostConfig := &HostConfig{}
|
||||
if err := container.Start(hostConfig); err != nil {
|
||||
complete <- err
|
||||
return
|
||||
}
|
||||
@@ -1201,3 +1204,35 @@ func BenchmarkRunParallel(b *testing.B) {
|
||||
b.Fatal(errors)
|
||||
}
|
||||
}
|
||||
|
||||
func tempDir(t *testing.T) string {
|
||||
tmpDir, err := ioutil.TempDir("", "docker-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tmpDir
|
||||
}
|
||||
|
||||
func TestBindMounts(t *testing.T) {
|
||||
r := mkRuntime(t)
|
||||
defer nuke(r)
|
||||
tmpDir := tempDir(t)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
writeFile(path.Join(tmpDir, "touch-me"), "", t)
|
||||
|
||||
// Test reading from a read-only bind mount
|
||||
stdout, _ := runContainer(r, []string{"-b", fmt.Sprintf("%s:/tmp:ro", tmpDir), "_", "ls", "/tmp"}, t)
|
||||
if !strings.Contains(stdout, "touch-me") {
|
||||
t.Fatal("Container failed to read from bind mount")
|
||||
}
|
||||
|
||||
// test writing to bind mount
|
||||
runContainer(r, []string{"-b", fmt.Sprintf("%s:/tmp:rw", tmpDir), "_", "touch", "/tmp/holla"}, t)
|
||||
readFile(path.Join(tmpDir, "holla"), t) // Will fail if the file doesn't exist
|
||||
|
||||
// test mounting to an illegal destination directory
|
||||
if _, err := runContainer(r, []string{"-b", fmt.Sprintf("%s:.", tmpDir), "ls", "."}, nil); err == nil {
|
||||
t.Fatal("Container bind mounted illegal directory")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var DOCKER_PATH string = path.Join(os.Getenv("DOCKERPATH"), "docker")
|
||||
var DOCKERPATH = path.Join(os.Getenv("DOCKERPATH"), "docker")
|
||||
|
||||
// WARNING: this crashTest will 1) crash your host, 2) remove all containers
|
||||
func runDaemon() (*exec.Cmd, error) {
|
||||
os.Remove("/var/run/docker.pid")
|
||||
exec.Command("rm", "-rf", "/var/lib/docker/containers").Run()
|
||||
cmd := exec.Command(DOCKER_PATH, "-d")
|
||||
cmd := exec.Command(DOCKERPATH, "-d")
|
||||
outPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -77,7 +77,7 @@ func crashTest() error {
|
||||
stop = false
|
||||
for i := 0; i < 100 && !stop; {
|
||||
func() error {
|
||||
cmd := exec.Command(DOCKER_PATH, "run", "base", "echo", fmt.Sprintf("%d", totalTestCount))
|
||||
cmd := exec.Command(DOCKERPATH, "run", "base", "echo", fmt.Sprintf("%d", totalTestCount))
|
||||
i++
|
||||
totalTestCount++
|
||||
outPipe, err := cmd.StdoutPipe()
|
||||
@@ -116,7 +116,6 @@ func crashTest() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Solomon Hykes <solomon@dotcloud.com>
|
||||
@@ -1,68 +0,0 @@
|
||||
# docker-build: build your software with docker
|
||||
|
||||
## Description
|
||||
|
||||
docker-build is a script to build docker images from source. It will be deprecated once the 'build' feature is incorporated into docker itself (See https://github.com/dotcloud/docker/issues/278)
|
||||
|
||||
Author: Solomon Hykes <solomon@dotcloud.com>
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
docker-builder requires:
|
||||
|
||||
1) A reasonably recent Python setup (tested on 2.7.2).
|
||||
|
||||
2) A running docker daemon at version 0.1.4 or more recent (http://www.docker.io/gettingstarted)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
First create a valid Changefile, which defines a sequence of changes to apply to a base image.
|
||||
|
||||
$ cat Changefile
|
||||
# Start build from a know base image
|
||||
from base:ubuntu-12.10
|
||||
# Update ubuntu sources
|
||||
run echo 'deb http://archive.ubuntu.com/ubuntu quantal main universe multiverse' > /etc/apt/sources.list
|
||||
run apt-get update
|
||||
# Install system packages
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang
|
||||
# Insert files from the host (./myscript must be present in the current directory)
|
||||
copy myscript /usr/local/bin/myscript
|
||||
|
||||
|
||||
Run docker-build, and pass the contents of your Changefile as standard input.
|
||||
|
||||
$ IMG=$(./docker-build < Changefile)
|
||||
|
||||
This will take a while: for each line of the changefile, docker-build will:
|
||||
|
||||
1. Create a new container to execute the given command or insert the given file
|
||||
2. Wait for the container to complete execution
|
||||
3. Commit the resulting changes as a new image
|
||||
4. Use the resulting image as the input of the next step
|
||||
|
||||
|
||||
If all the steps succeed, the result will be an image containing the combined results of each build step.
|
||||
You can trace back those build steps by inspecting the image's history:
|
||||
|
||||
$ docker history $IMG
|
||||
ID CREATED CREATED BY
|
||||
1e9e2045de86 A few seconds ago /bin/sh -c cat > /usr/local/bin/myscript; chmod +x /usr/local/bin/git
|
||||
77db140aa62a A few seconds ago /bin/sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang
|
||||
77db140aa62a A few seconds ago /bin/sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
77db140aa62a A few seconds ago /bin/sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
|
||||
83e85d155451 A few seconds ago /bin/sh -c apt-get update
|
||||
bfd53b36d9d3 A few seconds ago /bin/sh -c echo 'deb http://archive.ubuntu.com/ubuntu quantal main universe multiverse' > /etc/apt/sources.list
|
||||
base 2 weeks ago /bin/bash
|
||||
27cf78414709 2 weeks ago
|
||||
|
||||
|
||||
Note that your build started from 'base', as instructed by your Changefile. But that base image itself seems to have been built in 2 steps - hence the extra step in the history.
|
||||
|
||||
|
||||
You can use this build technique to create any image you want: a database, a web application, or anything else that can be build by a sequence of unix commands - in other words, anything else.
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# docker-build is a script to build docker images from source.
|
||||
# It will be deprecated once the 'build' feature is incorporated into docker itself.
|
||||
# (See https://github.com/dotcloud/docker/issues/278)
|
||||
#
|
||||
# Author: Solomon Hykes <solomon@dotcloud.com>
|
||||
|
||||
|
||||
|
||||
# First create a valid Changefile, which defines a sequence of changes to apply to a base image.
|
||||
#
|
||||
# $ cat Changefile
|
||||
# # Start build from a know base image
|
||||
# from base:ubuntu-12.10
|
||||
# # Update ubuntu sources
|
||||
# run echo 'deb http://archive.ubuntu.com/ubuntu quantal main universe multiverse' > /etc/apt/sources.list
|
||||
# run apt-get update
|
||||
# # Install system packages
|
||||
# run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
|
||||
# run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
# run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang
|
||||
# # Insert files from the host (./myscript must be present in the current directory)
|
||||
# copy myscript /usr/local/bin/myscript
|
||||
#
|
||||
#
|
||||
# Run docker-build, and pass the contents of your Changefile as standard input.
|
||||
#
|
||||
# $ IMG=$(./docker-build < Changefile)
|
||||
#
|
||||
# This will take a while: for each line of the changefile, docker-build will:
|
||||
#
|
||||
# 1. Create a new container to execute the given command or insert the given file
|
||||
# 2. Wait for the container to complete execution
|
||||
# 3. Commit the resulting changes as a new image
|
||||
# 4. Use the resulting image as the input of the next step
|
||||
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
def docker(args, stdin=None):
|
||||
print "# docker " + " ".join(args)
|
||||
p = subprocess.Popen(["docker"] + list(args), stdin=stdin, stdout=subprocess.PIPE)
|
||||
return p.stdout
|
||||
|
||||
def image_exists(img):
|
||||
return docker(["inspect", img]).read().strip() != ""
|
||||
|
||||
def image_config(img):
|
||||
return json.loads(docker(["inspect", img]).read()).get("config", {})
|
||||
|
||||
def run_and_commit(img_in, cmd, stdin=None, author=None, run=None):
|
||||
run_id = docker(["run"] + (["-i", "-a", "stdin"] if stdin else ["-d"]) + [img_in, "/bin/sh", "-c", cmd], stdin=stdin).read().rstrip()
|
||||
print "---> Waiting for " + run_id
|
||||
result=int(docker(["wait", run_id]).read().rstrip())
|
||||
if result != 0:
|
||||
print "!!! '{}' return non-zero exit code '{}'. Aborting.".format(cmd, result)
|
||||
sys.exit(1)
|
||||
return docker(["commit"] + (["-author", author] if author else []) + (["-run", json.dumps(run)] if run is not None else []) + [run_id]).read().rstrip()
|
||||
|
||||
def insert(base, src, dst, author=None):
|
||||
print "COPY {} to {} in {}".format(src, dst, base)
|
||||
if dst == "":
|
||||
raise Exception("Missing destination path")
|
||||
stdin = file(src)
|
||||
stdin.seek(0)
|
||||
return run_and_commit(base, "cat > {0}; chmod +x {0}".format(dst), stdin=stdin, author=author)
|
||||
|
||||
def add(base, src, dst, author=None):
|
||||
print "PUSH to {} in {}".format(dst, base)
|
||||
if src == ".":
|
||||
tar = subprocess.Popen(["tar", "-c", "."], stdout=subprocess.PIPE).stdout
|
||||
else:
|
||||
tar = subprocess.Popen(["curl", src], stdout=subprocess.PIPE).stdout
|
||||
if dst == "":
|
||||
raise Exception("Missing argument to push")
|
||||
return run_and_commit(base, "mkdir -p '{0}' && tar -C '{0}' -x".format(dst), stdin=tar, author=author)
|
||||
|
||||
def main():
|
||||
base=""
|
||||
maintainer=""
|
||||
steps = []
|
||||
try:
|
||||
for line in sys.stdin.readlines():
|
||||
line = line.strip()
|
||||
# Skip comments and empty lines
|
||||
if line == "" or line[0] == "#":
|
||||
continue
|
||||
op, param = line.split(None, 1)
|
||||
print op.upper() + " " + param
|
||||
if op == "from":
|
||||
base = param
|
||||
steps.append(base)
|
||||
elif op == "maintainer":
|
||||
maintainer = param
|
||||
elif op == "run":
|
||||
result = run_and_commit(base, param, author=maintainer)
|
||||
steps.append(result)
|
||||
base = result
|
||||
print "===> " + base
|
||||
elif op == "copy":
|
||||
src, dst = param.split(" ", 1)
|
||||
result = insert(base, src, dst, author=maintainer)
|
||||
steps.append(result)
|
||||
base = result
|
||||
print "===> " + base
|
||||
elif op == "add":
|
||||
src, dst = param.split(" ", 1)
|
||||
result = add(base, src, dst, author=maintainer)
|
||||
steps.append(result)
|
||||
base=result
|
||||
print "===> " + base
|
||||
elif op == "expose":
|
||||
config = image_config(base)
|
||||
if config.get("PortSpecs") is None:
|
||||
config["PortSpecs"] = []
|
||||
portspec = param.strip()
|
||||
config["PortSpecs"].append(portspec)
|
||||
result = run_and_commit(base, "# (nop) expose port {}".format(portspec), author=maintainer, run=config)
|
||||
steps.append(result)
|
||||
base=result
|
||||
print "===> " + base
|
||||
elif op == "cmd":
|
||||
config = image_config(base)
|
||||
cmd = list(json.loads(param))
|
||||
config["Cmd"] = cmd
|
||||
result = run_and_commit(base, "# (nop) set default command to '{}'".format(" ".join(cmd)), author=maintainer, run=config)
|
||||
steps.append(result)
|
||||
base=result
|
||||
print "===> " + base
|
||||
else:
|
||||
print "Skipping uknown op " + op
|
||||
except:
|
||||
docker(["rmi"] + steps[1:])
|
||||
raise
|
||||
print base
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,13 +0,0 @@
|
||||
# Start build from a know base image
|
||||
maintainer Solomon Hykes <solomon@dotcloud.com>
|
||||
from base:ubuntu-12.10
|
||||
# Update ubuntu sources
|
||||
run echo 'deb http://archive.ubuntu.com/ubuntu quantal main universe multiverse' > /etc/apt/sources.list
|
||||
run apt-get update
|
||||
# Install system packages
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang
|
||||
# Insert files from the host (./myscript must be present in the current directory)
|
||||
copy myscript /usr/local/bin/myscript
|
||||
push /src
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo hello, world!
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
echo "Ensuring basic dependencies are installed..."
|
||||
apt-get -qq update
|
||||
apt-get -qq install lxc wget bsdtar
|
||||
apt-get -qq install lxc wget
|
||||
|
||||
echo "Looking in /proc/filesystems to see if we have AUFS support..."
|
||||
if grep -q aufs /proc/filesystems
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Generate a very minimal filesystem based on busybox-static,
|
||||
# and load it into the local docker under the name "docker-ut".
|
||||
|
||||
missing_pkg() {
|
||||
echo "Sorry, I could not locate $1"
|
||||
echo "Try 'apt-get install ${2:-$1}'?"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BUSYBOX=$(which busybox)
|
||||
[ "$BUSYBOX" ] || missing_pkg busybox busybox-static
|
||||
SOCAT=$(which socat)
|
||||
[ "$SOCAT" ] || missing_pkg socat
|
||||
|
||||
shopt -s extglob
|
||||
set -ex
|
||||
ROOTFS=`mktemp -d /tmp/rootfs-busybox.XXXXXXXXXX`
|
||||
trap "rm -rf $ROOTFS" INT QUIT TERM
|
||||
cd $ROOTFS
|
||||
|
||||
mkdir bin etc dev dev/pts lib proc sys tmp
|
||||
touch etc/resolv.conf
|
||||
cp /etc/nsswitch.conf etc/nsswitch.conf
|
||||
echo root:x:0:0:root:/:/bin/sh > etc/passwd
|
||||
echo root:x:0: > etc/group
|
||||
ln -s lib lib64
|
||||
ln -s bin sbin
|
||||
cp $BUSYBOX $SOCAT bin
|
||||
for X in $(busybox --list)
|
||||
do
|
||||
ln -s busybox bin/$X
|
||||
done
|
||||
rm bin/init
|
||||
ln bin/busybox bin/init
|
||||
cp -P /lib/x86_64-linux-gnu/lib{pthread*,c*(-*),dl*(-*),nsl*(-*),nss_*,util*(-*),wrap,z}.so* lib
|
||||
cp /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 lib
|
||||
cp -P /usr/lib/x86_64-linux-gnu/lib{crypto,ssl}.so* lib
|
||||
for X in console null ptmx random stdin stdout stderr tty urandom zero
|
||||
do
|
||||
cp -a /dev/$X dev
|
||||
done
|
||||
|
||||
tar -cf- . | docker import - docker-ut
|
||||
docker run -i -u root docker-ut /bin/echo Success.
|
||||
rm -rf $ROOTFS
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
GIT_COMMIT string
|
||||
GITCOMMIT string
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -24,53 +24,50 @@ func main() {
|
||||
docker.SysInit()
|
||||
return
|
||||
}
|
||||
host := "127.0.0.1"
|
||||
port := 4243
|
||||
// FIXME: Switch d and D ? (to be more sshd like)
|
||||
flDaemon := flag.Bool("d", false, "Daemon mode")
|
||||
flDebug := flag.Bool("D", false, "Debug mode")
|
||||
flAutoRestart := flag.Bool("r", false, "Restart previously running containers")
|
||||
bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge")
|
||||
pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID")
|
||||
flHost := flag.String("H", fmt.Sprintf("%s:%d", host, port), "Host:port to bind/connect to")
|
||||
flGraphPath := flag.String("g", "/var/lib/docker", "Path to graph storage base dir.")
|
||||
flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.")
|
||||
flDns := flag.String("dns", "", "Set custom dns servers")
|
||||
flHosts := docker.ListOpts{fmt.Sprintf("tcp://%s:%d", docker.DEFAULTHTTPHOST, docker.DEFAULTHTTPPORT)}
|
||||
flag.Var(&flHosts, "H", "tcp://host:port to bind/connect to or unix://path/to/socket to use")
|
||||
flag.Parse()
|
||||
if len(flHosts) > 1 {
|
||||
flHosts = flHosts[1:len(flHosts)] //trick to display a nice defaul value in the usage
|
||||
}
|
||||
for i, flHost := range flHosts {
|
||||
flHosts[i] = utils.ParseHost(docker.DEFAULTHTTPHOST, docker.DEFAULTHTTPPORT, flHost)
|
||||
}
|
||||
|
||||
if *bridgeName != "" {
|
||||
docker.NetworkBridgeIface = *bridgeName
|
||||
} else {
|
||||
docker.NetworkBridgeIface = docker.DefaultNetworkBridge
|
||||
}
|
||||
|
||||
if strings.Contains(*flHost, ":") {
|
||||
hostParts := strings.Split(*flHost, ":")
|
||||
if len(hostParts) != 2 {
|
||||
log.Fatal("Invalid bind address format.")
|
||||
os.Exit(-1)
|
||||
}
|
||||
if hostParts[0] != "" {
|
||||
host = hostParts[0]
|
||||
}
|
||||
if p, err := strconv.Atoi(hostParts[1]); err == nil {
|
||||
port = p
|
||||
}
|
||||
} else {
|
||||
host = *flHost
|
||||
}
|
||||
|
||||
if *flDebug {
|
||||
os.Setenv("DEBUG", "1")
|
||||
}
|
||||
docker.GIT_COMMIT = GIT_COMMIT
|
||||
docker.GITCOMMIT = GITCOMMIT
|
||||
if *flDaemon {
|
||||
if flag.NArg() != 0 {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
if err := daemon(*pidfile, host, port, *flAutoRestart); err != nil {
|
||||
if err := daemon(*pidfile, *flGraphPath, flHosts, *flAutoRestart, *flEnableCors, *flDns); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
} else {
|
||||
if err := docker.ParseCommands(host, port, flag.Args()...); err != nil {
|
||||
if len(flHosts) > 1 {
|
||||
log.Fatal("Please specify only one -H")
|
||||
return
|
||||
}
|
||||
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)
|
||||
if err := docker.ParseCommands(protoAddrParts[0], protoAddrParts[1], flag.Args()...); err != nil {
|
||||
log.Fatal(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
@@ -104,10 +101,7 @@ func removePidFile(pidfile string) {
|
||||
}
|
||||
}
|
||||
|
||||
func daemon(pidfile, addr string, port int, autoRestart bool) error {
|
||||
if addr != "127.0.0.1" {
|
||||
log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
|
||||
}
|
||||
func daemon(pidfile string, flGraphPath string, protoAddrs []string, autoRestart, enableCors bool, flDns string) error {
|
||||
if err := createPidFile(pidfile); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -121,11 +115,36 @@ func daemon(pidfile, addr string, port int, autoRestart bool) error {
|
||||
removePidFile(pidfile)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
server, err := docker.NewServer(autoRestart)
|
||||
var dns []string
|
||||
if flDns != "" {
|
||||
dns = []string{flDns}
|
||||
}
|
||||
server, err := docker.NewServer(flGraphPath, autoRestart, enableCors, dns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return docker.ListenAndServe(fmt.Sprintf("%s:%d", addr, port), server, true)
|
||||
chErrors := make(chan error, len(protoAddrs))
|
||||
for _, protoAddr := range protoAddrs {
|
||||
protoAddrParts := strings.SplitN(protoAddr, "://", 2)
|
||||
if protoAddrParts[0] == "unix" {
|
||||
syscall.Unlink(protoAddrParts[1])
|
||||
} else if protoAddrParts[0] == "tcp" {
|
||||
if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") {
|
||||
log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
|
||||
}
|
||||
} else {
|
||||
log.Fatal("Invalid protocol format.")
|
||||
os.Exit(-1)
|
||||
}
|
||||
go func() {
|
||||
chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true)
|
||||
}()
|
||||
}
|
||||
for i := 0; i < len(protoAddrs); i += 1 {
|
||||
err := <-chErrors
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -46,12 +46,11 @@ clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
docs:
|
||||
#-rm -rf $(BUILDDIR)/*
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The documentation pages are now in $(BUILDDIR)/html."
|
||||
|
||||
server:
|
||||
server: docs
|
||||
@cd $(BUILDDIR)/html; $(PYTHON) -m SimpleHTTPServer 8000
|
||||
|
||||
site:
|
||||
@@ -62,12 +61,13 @@ site:
|
||||
|
||||
connect:
|
||||
@echo connecting dotcloud to www.docker.io website, make sure to use user 1
|
||||
@cd _build/website/ ; \
|
||||
dotcloud connect dockerwebsite ;
|
||||
@echo or create your own "dockerwebsite" app
|
||||
@cd $(BUILDDIR)/website/ ; \
|
||||
dotcloud connect dockerwebsite ; \
|
||||
dotcloud list
|
||||
|
||||
push:
|
||||
@cd _build/website/ ; \
|
||||
@cd $(BUILDDIR)/website/ ; \
|
||||
dotcloud push
|
||||
|
||||
$(VERSIONS):
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
This directory holds the authoritative specifications of APIs defined and implemented by Docker. Currently this includes:
|
||||
|
||||
* The remote API by which a docker node can be queried over HTTP
|
||||
* The registry API by which a docker node can download and upload container images for storage and sharing
|
||||
* The index search API by which a docker node can search the public index for images to download
|
||||
@@ -5,13 +5,14 @@
|
||||
APIs
|
||||
====
|
||||
|
||||
This following :
|
||||
Your programs and scripts can access Docker's functionality via these interfaces:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
registry_index_spec
|
||||
registry_api
|
||||
index_search_api
|
||||
index_api
|
||||
docker_remote_api
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
:title: Index API
|
||||
:description: API Documentation for Docker Index
|
||||
:keywords: API, Docker, index, REST, documentation
|
||||
|
||||
=================
|
||||
Docker Index API
|
||||
=================
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. Brief introduction
|
||||
=====================
|
||||
|
||||
- This is the REST API for the Docker index
|
||||
- Authorization is done with basic auth over SSL
|
||||
- Not all commands require authentication, only those noted as such.
|
||||
|
||||
2. Endpoints
|
||||
============
|
||||
|
||||
2.1 Repository
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Repositories
|
||||
*************
|
||||
|
||||
User Repo
|
||||
~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/
|
||||
|
||||
Create a user repository with the given ``namespace`` and ``repo_name``.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repo_name)/
|
||||
|
||||
Delete a user repository with the given ``namespace`` and ``repo_name``.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
""
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Deleted
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
Library Repo
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/
|
||||
|
||||
Create a library repository with the given ``repo_name``.
|
||||
This is a restricted feature only available to docker admins.
|
||||
|
||||
When namespace is missing, it is assumed to be ``library``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=write
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
.. http:delete:: /v1/repositories/(repo_name)/
|
||||
|
||||
Delete a library repository with the given ``repo_name``.
|
||||
This is a restricted feature only available to docker admins.
|
||||
|
||||
When namespace is missing, it is assumed to be ``library``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foobar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
""
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=delete
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Deleted
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
Repository Images
|
||||
*****************
|
||||
|
||||
User Repo Images
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/images
|
||||
|
||||
Update the images for a user repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active or permission denied
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(namespace)/(repo_name)/images
|
||||
|
||||
get the images for a user repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foo/bar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”},
|
||||
{“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”,
|
||||
“checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 404: Not found
|
||||
|
||||
Library Repo Images
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/images
|
||||
|
||||
Update the images for a library repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active or permission denied
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(repo_name)/images
|
||||
|
||||
get the images for a library repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foobar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”},
|
||||
{“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”,
|
||||
“checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
Repository Authorization
|
||||
************************
|
||||
|
||||
Library Repo
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/auth
|
||||
|
||||
authorize a token for a library repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/auth HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Token signature=123abc,repository="library/foobar",access=write
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"OK"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 403: Permission denied
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
User Repo
|
||||
~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/auth
|
||||
|
||||
authorize a token for a user repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/auth HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Token signature=123abc,repository="foo/bar",access=write
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"OK"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 403: Permission denied
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
2.2 Users
|
||||
^^^^^^^^^
|
||||
|
||||
User Login
|
||||
**********
|
||||
|
||||
.. http:get:: /v1/users
|
||||
|
||||
If you want to check your login, you can try this endpoint
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/users HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
OK
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
|
||||
User Register
|
||||
*************
|
||||
|
||||
.. http:post:: /v1/users
|
||||
|
||||
Registering a new account.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v1/users HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{"email": "sam@dotcloud.com",
|
||||
"password": "toto42",
|
||||
"username": "foobar"'}
|
||||
|
||||
:jsonparameter email: valid email address, that needs to be confirmed
|
||||
:jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
:jsonparameter password: min 5 characters
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"User Created"
|
||||
|
||||
:statuscode 201: User Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
|
||||
Update User
|
||||
***********
|
||||
|
||||
.. http:put:: /v1/users/(username)/
|
||||
|
||||
Change a password or email address for given user. If you pass in an email,
|
||||
it will add it to your account, it will not remove the old one. Passwords will
|
||||
be updated.
|
||||
|
||||
It is up to the client to verify that that password that is sent is the one that
|
||||
they want. Common approach is to have them type it twice.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/users/fakeuser/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
{"email": "sam@dotcloud.com",
|
||||
"password": "toto42"}
|
||||
|
||||
:parameter username: username for the person you want to update
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: User Updated
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
:statuscode 404: User not found
|
||||
|
||||
|
||||
2.3 Search
|
||||
^^^^^^^^^^
|
||||
If you need to search the index, this is the endpoint you would use.
|
||||
|
||||
Search
|
||||
******
|
||||
|
||||
.. http:get:: /v1/search
|
||||
|
||||
Search the Index given a search term. It accepts :http:method:`get` only.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/search?q=search_term HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{"query":"search_term",
|
||||
"num_results": 2,
|
||||
"results" : [
|
||||
{"name": "dotcloud/base", "description": "A base ubuntu64 image..."},
|
||||
{"name": "base2", "description": "A base ubuntu64 image..."},
|
||||
]
|
||||
}
|
||||
|
||||
:query q: what you want to search for
|
||||
:statuscode 200: no error
|
||||
:statuscode 500: server error
|
||||
@@ -1,43 +0,0 @@
|
||||
:title: Docker Index documentation
|
||||
:description: Documentation for docker Index
|
||||
:keywords: docker, index, api
|
||||
|
||||
|
||||
=======================
|
||||
Docker Index Search API
|
||||
=======================
|
||||
|
||||
Search
|
||||
------
|
||||
|
||||
.. http:get:: /v1/search
|
||||
|
||||
Search the Index given a search term. It accepts :http:method:`get` only.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/search?q=search_term HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{"query":"search_term",
|
||||
"num_results": 2,
|
||||
"results" : [
|
||||
{"name": "dotcloud/base", "description": "A base ubuntu64 image..."},
|
||||
{"name": "base2", "description": "A base ubuntu64 image..."},
|
||||
]
|
||||
}
|
||||
|
||||
:query q: what you want to search for
|
||||
:statuscode 200: no error
|
||||
:statuscode 500: server error
|
||||
@@ -1,7 +1,6 @@
|
||||
:title: Registry Documentation
|
||||
:description: Documentation for docker Registry and Registry API
|
||||
:keywords: docker, registry, api, index
|
||||
|
||||
:title: Registry API
|
||||
:description: API Documentation for Docker Registry
|
||||
:keywords: API, Docker, index, registry, REST, documentation
|
||||
|
||||
===================
|
||||
Docker Registry API
|
||||
@@ -9,29 +8,10 @@ Docker Registry API
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. The 3 roles
|
||||
===============
|
||||
1. Brief introduction
|
||||
=====================
|
||||
|
||||
1.1 Index
|
||||
---------
|
||||
|
||||
The Index is responsible for centralizing information about:
|
||||
- User accounts
|
||||
- Checksums of the images
|
||||
- Public namespaces
|
||||
|
||||
The Index has different components:
|
||||
- Web UI
|
||||
- Meta-data store (comments, stars, list public repositories)
|
||||
- Authentication service
|
||||
- Tokenization
|
||||
|
||||
The index is authoritative for those information.
|
||||
|
||||
We expect that there will be only one instance of the index, run and managed by dotCloud.
|
||||
|
||||
1.2 Registry
|
||||
------------
|
||||
- This is the REST API for the Docker Registry
|
||||
- It stores the images and the graph for a set of repositories
|
||||
- It does not have user accounts data
|
||||
- It has no notion of user accounts or authorization
|
||||
@@ -60,418 +40,424 @@ We expect that there will be multiple registries out there. To help to grasp the
|
||||
|
||||
The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys).
|
||||
|
||||
1.3 Docker
|
||||
2. Endpoints
|
||||
============
|
||||
|
||||
2.1 Images
|
||||
----------
|
||||
|
||||
On top of being a runtime for LXC, Docker is the Registry client. It supports:
|
||||
- Push / Pull on the registry
|
||||
- Client authentication on the Index
|
||||
Layer
|
||||
*****
|
||||
|
||||
2. Workflow
|
||||
===========
|
||||
.. http:get:: /v1/images/(image_id)/layer
|
||||
|
||||
2.1 Pull
|
||||
get image layer for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Token akmklmasadalkmsdfgsdgdge33
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
{
|
||||
id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c",
|
||||
parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f",
|
||||
created: "2013-04-30T17:46:10.843673+03:00",
|
||||
container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7",
|
||||
container_config: {
|
||||
Hostname: "host-test",
|
||||
User: "",
|
||||
Memory: 0,
|
||||
MemorySwap: 0,
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
PortSpecs: null,
|
||||
Tty: false,
|
||||
OpenStdin: false,
|
||||
StdinOnce: false,
|
||||
Env: null,
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"apt-get -q -yy -f install libevent-dev"
|
||||
],
|
||||
Dns: null,
|
||||
Image: "imagename/blah",
|
||||
Volumes: { },
|
||||
VolumesFrom: ""
|
||||
},
|
||||
docker_version: "0.1.7"
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
.. http:put:: /v1/images/(image_id)/layer
|
||||
|
||||
put image layer for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Token akmklmasadalkmsdfgsdgdge33
|
||||
|
||||
{
|
||||
id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c",
|
||||
parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f",
|
||||
created: "2013-04-30T17:46:10.843673+03:00",
|
||||
container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7",
|
||||
container_config: {
|
||||
Hostname: "host-test",
|
||||
User: "",
|
||||
Memory: 0,
|
||||
MemorySwap: 0,
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
PortSpecs: null,
|
||||
Tty: false,
|
||||
OpenStdin: false,
|
||||
StdinOnce: false,
|
||||
Env: null,
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"apt-get -q -yy -f install libevent-dev"
|
||||
],
|
||||
Dns: null,
|
||||
Image: "imagename/blah",
|
||||
Volumes: { },
|
||||
VolumesFrom: ""
|
||||
},
|
||||
docker_version: "0.1.7"
|
||||
}
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
Image
|
||||
*****
|
||||
|
||||
.. http:put:: /v1/images/(image_id)/json
|
||||
|
||||
put image for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
{
|
||||
“id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”,
|
||||
“checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
|
||||
.. http:get:: /v1/images/(image_id)/json
|
||||
|
||||
get image for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
“id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”,
|
||||
“checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
Ancestry
|
||||
********
|
||||
|
||||
.. http:get:: /v1/images/(image_id)/ancestry
|
||||
|
||||
get ancestry for an image given an ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/ancestry HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
["088b4502f51920fbd9b7c503e87c7a2c05aa3adc3d35e79c031fa126b403200f",
|
||||
"aeee63968d87c7da4a5cf5d2be6bee4e21bc226fd62273d180a49c96c62e4543",
|
||||
"bfa4c5326bc764280b0863b46a4b20d940bc1897ef9c1dfec060604bdc383280",
|
||||
"6ab5893c6927c15a15665191f2c6cf751f5056d8b95ceee32e43c5e8a3648544"]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
2.2 Tags
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_pull_chart.png
|
||||
.. http:get:: /v1/repositories/(namespace)/(repository)/tags
|
||||
|
||||
1. Contact the Index to know where I should download “samalba/busybox”
|
||||
2. Index replies:
|
||||
a. “samalba/busybox” is on Registry A
|
||||
b. here are the checksums for “samalba/busybox” (for all layers)
|
||||
c. token
|
||||
3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location.
|
||||
4. registry contacts index to verify if token/user is allowed to download images
|
||||
5. Index returns true/false lettings registry know if it should proceed or error out
|
||||
6. Get the payload for all layers
|
||||
get all of the tags for the given repo.
|
||||
|
||||
It’s possible to run docker pull \https://<registry>/repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks.
|
||||
**Example Request**:
|
||||
|
||||
Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage.
|
||||
.. sourcecode:: http
|
||||
|
||||
Token is only returned when the 'X-Docker-Token' header is sent with request.
|
||||
|
||||
Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account.
|
||||
|
||||
API (pulling repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
X-Docker-Token: true
|
||||
**Action**:
|
||||
(looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1)
|
||||
|
||||
2. (Index -> Docker) HTTP 200 OK
|
||||
|
||||
**Headers**:
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
**Body**:
|
||||
Jsonified checksums (see part 4.4.1)
|
||||
|
||||
3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry -> Index) GET /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
**Body**:
|
||||
<ids and checksums in payload>
|
||||
|
||||
**Action**:
|
||||
( Lookup token see if they have access to pull.)
|
||||
|
||||
If good:
|
||||
HTTP 200 OK
|
||||
Index will invalidate the token
|
||||
If bad:
|
||||
HTTP 401 Unauthorized
|
||||
|
||||
5. (Docker -> Registry) GET /v1/images/928374982374/ancestry
|
||||
**Action**:
|
||||
(for each image id returned in the registry, fetch /json + /layer)
|
||||
|
||||
.. note::
|
||||
|
||||
If someone makes a second request, then we will always give a new token, never reuse tokens.
|
||||
|
||||
2.2 Push
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_push_chart.png
|
||||
|
||||
1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index)
|
||||
3. Push the image on the registry (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images)
|
||||
6. docker contacts the index to give checksums for upload images
|
||||
|
||||
.. note::
|
||||
|
||||
**It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed.
|
||||
|
||||
.. note::
|
||||
|
||||
**Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies.
|
||||
|
||||
Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end).
|
||||
|
||||
API (pushing repos foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) PUT /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we allocated a new repository, and set to initialized
|
||||
|
||||
**Body**::
|
||||
(The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push)::
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
2. (Index -> Docker) 200 Created
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
|
||||
3. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry->Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
grants a session (if token is approved) and fetches the images id
|
||||
|
||||
5. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**::
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- Cookie: (Cookie provided by the Registry)
|
||||
|
||||
6. (Docker -> Registry) PUT /v1/images/98765432/json
|
||||
**Headers**:
|
||||
GET /v1/repositories/foo/bar/tags HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer
|
||||
**Headers**:
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Repository not found
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
get a tag for the given repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
8. (Docker -> Registry) PUT /v1/images/98765432/layer
|
||||
**Headers**:
|
||||
X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to get
|
||||
|
||||
9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Tag not found
|
||||
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
delete the tag for the repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
**Body**:
|
||||
“98765432”
|
||||
|
||||
10. (Docker -> Index) PUT /v1/repositories/foo/bar/images
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to delete
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry1.docker.io (no validation on this right now)
|
||||
**Example Response**:
|
||||
|
||||
**Body**:
|
||||
(The image, id’s, tags and checksums)
|
||||
.. sourcecode:: http
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
**Return** HTTP 204
|
||||
""
|
||||
|
||||
.. note::
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Tag not found
|
||||
|
||||
If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells.
|
||||
|
||||
If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token.
|
||||
.. http:put:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
3. How to use the Registry in standalone mode
|
||||
=============================================
|
||||
put a tag for the given repo.
|
||||
|
||||
The Index has two main purposes (along with its fancy social features):
|
||||
**Example Request**:
|
||||
|
||||
- Resolve short names (to avoid passing absolute URLs all the time)
|
||||
- username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
|
||||
- Authenticate a user as a repos owner (for a central referenced repository)
|
||||
.. sourcecode:: http
|
||||
|
||||
3.1 Without an Index
|
||||
--------------------
|
||||
Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud.
|
||||
PUT /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...).
|
||||
“9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”
|
||||
|
||||
In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity.
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to add
|
||||
|
||||
As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary).
|
||||
**Example Response**:
|
||||
|
||||
3.2 With an Index
|
||||
-----------------
|
||||
.. sourcecode:: http
|
||||
|
||||
The Index data needed by the Registry are simple:
|
||||
- Serve the checksums
|
||||
- Provide and authorize a Token
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index.
|
||||
""
|
||||
|
||||
The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers.
|
||||
:statuscode 200: OK
|
||||
:statuscode 400: Invalid data
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
4. The API
|
||||
==========
|
||||
2.3 Repositories
|
||||
----------------
|
||||
|
||||
The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repository)/
|
||||
|
||||
4.1 Images
|
||||
----------
|
||||
delete a repository
|
||||
|
||||
The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them.
|
||||
**Example Request**:
|
||||
|
||||
The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty.
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/<image_id>/layer
|
||||
PUT /v1/images/<image_id>/layer
|
||||
GET /v1/images/<image_id>/json
|
||||
PUT /v1/images/<image_id>/json
|
||||
GET /v1/images/<image_id>/ancestry
|
||||
PUT /v1/images/<image_id>/ancestry
|
||||
DELETE /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
4.2 Users
|
||||
---------
|
||||
""
|
||||
|
||||
4.2.1 Create a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
|
||||
POST /v1/users
|
||||
**Example Response**:
|
||||
|
||||
**Body**:
|
||||
{"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
|
||||
.. sourcecode:: http
|
||||
|
||||
**Validation**:
|
||||
- **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
- **password**: min 5 characters
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
**Valid**: return HTTP 200
|
||||
""
|
||||
|
||||
Errors: HTTP 400 (we should create error codes for possible errors)
|
||||
- invalid json
|
||||
- missing field
|
||||
- wrong format (username, password, email, etc)
|
||||
- forbidden name
|
||||
- name already exists
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Repository not found
|
||||
|
||||
.. note::
|
||||
3.0 Authorization
|
||||
=================
|
||||
This is where we describe the authorization process, including the tokens and cookies.
|
||||
|
||||
A user account will be valid only if the email has been validated (a validation link is sent to the email address).
|
||||
|
||||
4.2.2 Update a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PUT /v1/users/<username>
|
||||
|
||||
**Body**:
|
||||
{"password": "toto"}
|
||||
|
||||
.. note::
|
||||
|
||||
We can also update email address, if they do, they will need to reverify their new email address.
|
||||
|
||||
4.2.3 Login (Index)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future.
|
||||
|
||||
GET /v1/users
|
||||
|
||||
**Return**:
|
||||
- Valid: HTTP 200
|
||||
- Invalid login: HTTP 401
|
||||
- Account inactive: HTTP 403 Account is not Active
|
||||
|
||||
4.3 Tags (Registry)
|
||||
-------------------
|
||||
|
||||
The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API.
|
||||
|
||||
The following naming restrictions apply:
|
||||
|
||||
- Namespaces must match the same regular expression as usernames (See 4.2.1.)
|
||||
- Repository names must match the regular expression [a-zA-Z0-9-_.]
|
||||
|
||||
4.3.1 Get all tags
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repository_name>/tags
|
||||
|
||||
**Return**: HTTP 200
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
4.3.2 Read the content of a tag (resolve the image id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
**Return**:
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
4.3.3 Delete a tag (registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
4.4 Images (Index)
|
||||
------------------
|
||||
|
||||
For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository.
|
||||
|
||||
4.4.1 Get the images
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Return**: HTTP 200
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
|
||||
4.4.2 Add/update the images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You always add images, you never remove them.
|
||||
|
||||
PUT /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Body**:
|
||||
[ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ]
|
||||
|
||||
**Return** 204
|
||||
|
||||
5. Chaining Registries
|
||||
======================
|
||||
|
||||
It’s possible to chain Registries server for several reasons:
|
||||
- Load balancing
|
||||
- Delegate the next request to another server
|
||||
|
||||
When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download.
|
||||
|
||||
The Index and Registry use this mechanism to redirect on one or the other.
|
||||
|
||||
Example with an image download:
|
||||
On every request, a special header can be returned:
|
||||
|
||||
X-Docker-Endpoints: server1,server2
|
||||
|
||||
On the next request, the client will always pick a server from this list.
|
||||
|
||||
6. Authentication & Authorization
|
||||
=================================
|
||||
|
||||
6.1 On the Index
|
||||
-----------------
|
||||
|
||||
The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Basic realm="auth required",Token
|
||||
|
||||
You have 3 options:
|
||||
|
||||
1. Provide user credentials and ask for a token
|
||||
|
||||
**Header**:
|
||||
- Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
- X-Docker-Token: true
|
||||
|
||||
In this case, along with the 200 response, you’ll get a new token (if user auth is ok):
|
||||
If authorization isn't correct you get a 401 response.
|
||||
If account isn't active you will get a 403 response.
|
||||
|
||||
**Response**:
|
||||
- 200 OK
|
||||
- X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
2. Provide user credentials only
|
||||
|
||||
**Header**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
|
||||
3. Provide Token
|
||||
|
||||
**Header**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
6.2 On the Registry
|
||||
-------------------
|
||||
|
||||
The Registry only supports the Token challenge::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Token
|
||||
|
||||
The only way is to provide a token on “401 Unauthorized” responses::
|
||||
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.::
|
||||
|
||||
200 OK
|
||||
Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
|
||||
|
||||
Next request::
|
||||
|
||||
GET /(...)
|
||||
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="
|
||||
TODO: add more info.
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
:title: Registry Documentation
|
||||
:description: Documentation for docker Registry and Registry API
|
||||
:keywords: docker, registry, api, index
|
||||
|
||||
|
||||
=====================
|
||||
Registry & index Spec
|
||||
=====================
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. The 3 roles
|
||||
===============
|
||||
|
||||
1.1 Index
|
||||
---------
|
||||
|
||||
The Index is responsible for centralizing information about:
|
||||
- User accounts
|
||||
- Checksums of the images
|
||||
- Public namespaces
|
||||
|
||||
The Index has different components:
|
||||
- Web UI
|
||||
- Meta-data store (comments, stars, list public repositories)
|
||||
- Authentication service
|
||||
- Tokenization
|
||||
|
||||
The index is authoritative for those information.
|
||||
|
||||
We expect that there will be only one instance of the index, run and managed by dotCloud.
|
||||
|
||||
1.2 Registry
|
||||
------------
|
||||
- It stores the images and the graph for a set of repositories
|
||||
- It does not have user accounts data
|
||||
- It has no notion of user accounts or authorization
|
||||
- It delegates authentication and authorization to the Index Auth service using tokens
|
||||
- It supports different storage backends (S3, cloud files, local FS)
|
||||
- It doesn’t have a local database
|
||||
- It will be open-sourced at some point
|
||||
|
||||
We expect that there will be multiple registries out there. To help to grasp the context, here are some examples of registries:
|
||||
|
||||
- **sponsor registry**: such a registry is provided by a third-party hosting infrastructure as a convenience for their customers and the docker community as a whole. Its costs are supported by the third party, but the management and operation of the registry are supported by dotCloud. It features read/write access, and delegates authentication and authorization to the Index.
|
||||
- **mirror registry**: such a registry is provided by a third-party hosting infrastructure but is targeted at their customers only. Some mechanism (unspecified to date) ensures that public images are pulled from a sponsor registry to the mirror registry, to make sure that the customers of the third-party provider can “docker pull” those images locally.
|
||||
- **vendor registry**: such a registry is provided by a software vendor, who wants to distribute docker images. It would be operated and managed by the vendor. Only users authorized by the vendor would be able to get write access. Some images would be public (accessible for anyone), others private (accessible only for authorized users). Authentication and authorization would be delegated to the Index. The goal of vendor registries is to let someone do “docker pull basho/riak1.3” and automatically push from the vendor registry (instead of a sponsor registry); i.e. get all the convenience of a sponsor registry, while retaining control on the asset distribution.
|
||||
- **private registry**: such a registry is located behind a firewall, or protected by an additional security layer (HTTP authorization, SSL client-side certificates, IP address authorization...). The registry is operated by a private entity, outside of dotCloud’s control. It can optionally delegate additional authorization to the Index, but it is not mandatory.
|
||||
|
||||
.. note::
|
||||
|
||||
Mirror registries and private registries which do not use the Index don’t even need to run the registry code. They can be implemented by any kind of transport implementing HTTP GET and PUT. Read-only registries can be powered by a simple static HTTP server.
|
||||
|
||||
.. note::
|
||||
|
||||
The latter implies that while HTTP is the protocol of choice for a registry, multiple schemes are possible (and in some cases, trivial):
|
||||
- HTTP with GET (and PUT for read-write registries);
|
||||
- local mount point;
|
||||
- remote docker addressed through SSH.
|
||||
|
||||
The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys).
|
||||
|
||||
1.3 Docker
|
||||
----------
|
||||
|
||||
On top of being a runtime for LXC, Docker is the Registry client. It supports:
|
||||
- Push / Pull on the registry
|
||||
- Client authentication on the Index
|
||||
|
||||
2. Workflow
|
||||
===========
|
||||
|
||||
2.1 Pull
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_pull_chart.png
|
||||
|
||||
1. Contact the Index to know where I should download “samalba/busybox”
|
||||
2. Index replies:
|
||||
a. “samalba/busybox” is on Registry A
|
||||
b. here are the checksums for “samalba/busybox” (for all layers)
|
||||
c. token
|
||||
3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location.
|
||||
4. registry contacts index to verify if token/user is allowed to download images
|
||||
5. Index returns true/false lettings registry know if it should proceed or error out
|
||||
6. Get the payload for all layers
|
||||
|
||||
It’s possible to run docker pull \https://<registry>/repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks.
|
||||
|
||||
Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage.
|
||||
|
||||
Token is only returned when the 'X-Docker-Token' header is sent with request.
|
||||
|
||||
Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account.
|
||||
|
||||
API (pulling repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
X-Docker-Token: true
|
||||
**Action**:
|
||||
(looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1)
|
||||
|
||||
2. (Index -> Docker) HTTP 200 OK
|
||||
|
||||
**Headers**:
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
**Body**:
|
||||
Jsonified checksums (see part 4.4.1)
|
||||
|
||||
3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry -> Index) GET /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
**Body**:
|
||||
<ids and checksums in payload>
|
||||
|
||||
**Action**:
|
||||
( Lookup token see if they have access to pull.)
|
||||
|
||||
If good:
|
||||
HTTP 200 OK
|
||||
Index will invalidate the token
|
||||
If bad:
|
||||
HTTP 401 Unauthorized
|
||||
|
||||
5. (Docker -> Registry) GET /v1/images/928374982374/ancestry
|
||||
**Action**:
|
||||
(for each image id returned in the registry, fetch /json + /layer)
|
||||
|
||||
.. note::
|
||||
|
||||
If someone makes a second request, then we will always give a new token, never reuse tokens.
|
||||
|
||||
2.2 Push
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_push_chart.png
|
||||
|
||||
1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index)
|
||||
3. Push the image on the registry (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images)
|
||||
6. docker contacts the index to give checksums for upload images
|
||||
|
||||
.. note::
|
||||
|
||||
**It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed.
|
||||
|
||||
.. note::
|
||||
|
||||
**Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies.
|
||||
|
||||
Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end).
|
||||
|
||||
API (pushing repos foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) PUT /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we allocated a new repository, and set to initialized
|
||||
|
||||
**Body**::
|
||||
(The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push)::
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
2. (Index -> Docker) 200 Created
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
|
||||
3. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry->Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
grants a session (if token is approved) and fetches the images id
|
||||
|
||||
5. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**::
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- Cookie: (Cookie provided by the Registry)
|
||||
|
||||
6. (Docker -> Registry) PUT /v1/images/98765432/json
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
8. (Docker -> Registry) PUT /v1/images/98765432/layer
|
||||
**Headers**:
|
||||
X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh
|
||||
|
||||
9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
**Body**:
|
||||
“98765432”
|
||||
|
||||
10. (Docker -> Index) PUT /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry1.docker.io (no validation on this right now)
|
||||
|
||||
**Body**:
|
||||
(The image, id’s, tags and checksums)
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
**Return** HTTP 204
|
||||
|
||||
.. note::
|
||||
|
||||
If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells.
|
||||
|
||||
If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token.
|
||||
|
||||
2.3 Delete
|
||||
----------
|
||||
|
||||
If you need to delete something from the index or registry, we need a nice clean way to do that. Here is the workflow.
|
||||
|
||||
1. Docker contacts the index to request a delete of a repository “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and repository is valid, “samalba/busybox” is marked as deleted and a temporary token is returned
|
||||
3. Send a delete request to the registry for the repository (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A deletes the repository and everything associated to it.
|
||||
6. docker contacts the index to let it know it was removed from the registry, the index removes all records from the database.
|
||||
|
||||
.. note::
|
||||
|
||||
The Docker client should present an "Are you sure?" prompt to confirm the deletion before starting the process. Once it starts it can't be undone.
|
||||
|
||||
API (deleting repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) DELETE /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we make sure it is a valid repository, and set to deleted (logically)
|
||||
|
||||
**Body**::
|
||||
Empty
|
||||
|
||||
2. (Index -> Docker) 202 Accepted
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] # list of endpoints where this repo lives.
|
||||
|
||||
3. (Docker -> Registry) DELETE /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
|
||||
4. (Registry->Index) PUT /v1/repositories/foo/bar/auth
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
deletes the repository (if token is approved)
|
||||
|
||||
5. (Registry -> Docker) 200 OK
|
||||
200 If success
|
||||
403 if forbidden
|
||||
400 if bad request
|
||||
404 if repository isn't found
|
||||
|
||||
6. (Docker -> Index) DELETE /v1/repositories/foo/bar/
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry-1.docker.io (no validation on this right now)
|
||||
|
||||
**Body**:
|
||||
Empty
|
||||
|
||||
**Return** HTTP 200
|
||||
|
||||
|
||||
3. How to use the Registry in standalone mode
|
||||
=============================================
|
||||
|
||||
The Index has two main purposes (along with its fancy social features):
|
||||
|
||||
- Resolve short names (to avoid passing absolute URLs all the time)
|
||||
- username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
|
||||
- team/projectname -> \https://registry.docker.io/team/<team>/repositories/<projectname>/
|
||||
- Authenticate a user as a repos owner (for a central referenced repository)
|
||||
|
||||
3.1 Without an Index
|
||||
--------------------
|
||||
Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud.
|
||||
|
||||
In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...).
|
||||
|
||||
In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity.
|
||||
|
||||
As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary).
|
||||
|
||||
3.2 With an Index
|
||||
-----------------
|
||||
|
||||
The Index data needed by the Registry are simple:
|
||||
- Serve the checksums
|
||||
- Provide and authorize a Token
|
||||
|
||||
In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index.
|
||||
|
||||
The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers.
|
||||
|
||||
4. The API
|
||||
==========
|
||||
|
||||
The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md
|
||||
|
||||
4.1 Images
|
||||
----------
|
||||
|
||||
The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them.
|
||||
|
||||
The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty.
|
||||
|
||||
GET /v1/images/<image_id>/layer
|
||||
PUT /v1/images/<image_id>/layer
|
||||
GET /v1/images/<image_id>/json
|
||||
PUT /v1/images/<image_id>/json
|
||||
GET /v1/images/<image_id>/ancestry
|
||||
PUT /v1/images/<image_id>/ancestry
|
||||
|
||||
4.2 Users
|
||||
---------
|
||||
|
||||
4.2.1 Create a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
POST /v1/users
|
||||
|
||||
**Body**:
|
||||
{"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
|
||||
|
||||
**Validation**:
|
||||
- **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
- **password**: min 5 characters
|
||||
|
||||
**Valid**: return HTTP 200
|
||||
|
||||
Errors: HTTP 400 (we should create error codes for possible errors)
|
||||
- invalid json
|
||||
- missing field
|
||||
- wrong format (username, password, email, etc)
|
||||
- forbidden name
|
||||
- name already exists
|
||||
|
||||
.. note::
|
||||
|
||||
A user account will be valid only if the email has been validated (a validation link is sent to the email address).
|
||||
|
||||
4.2.2 Update a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PUT /v1/users/<username>
|
||||
|
||||
**Body**:
|
||||
{"password": "toto"}
|
||||
|
||||
.. note::
|
||||
|
||||
We can also update email address, if they do, they will need to reverify their new email address.
|
||||
|
||||
4.2.3 Login (Index)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future.
|
||||
|
||||
GET /v1/users
|
||||
|
||||
**Return**:
|
||||
- Valid: HTTP 200
|
||||
- Invalid login: HTTP 401
|
||||
- Account inactive: HTTP 403 Account is not Active
|
||||
|
||||
4.3 Tags (Registry)
|
||||
-------------------
|
||||
|
||||
The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API.
|
||||
|
||||
The following naming restrictions apply:
|
||||
|
||||
- Namespaces must match the same regular expression as usernames (See 4.2.1.)
|
||||
- Repository names must match the regular expression [a-zA-Z0-9-_.]
|
||||
|
||||
4.3.1 Get all tags
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repository_name>/tags
|
||||
|
||||
**Return**: HTTP 200
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
4.3.2 Read the content of a tag (resolve the image id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
**Return**:
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
4.3.3 Delete a tag (registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
4.4 Images (Index)
|
||||
------------------
|
||||
|
||||
For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository.
|
||||
|
||||
4.4.1 Get the images
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Return**: HTTP 200
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
|
||||
4.4.2 Add/update the images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You always add images, you never remove them.
|
||||
|
||||
PUT /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Body**:
|
||||
[ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ]
|
||||
|
||||
**Return** 204
|
||||
|
||||
4.5 Repositories
|
||||
----------------
|
||||
|
||||
4.5.1 Remove a Repository (Registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>
|
||||
|
||||
Return 200 OK
|
||||
|
||||
4.5.2 Remove a Repository (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
This starts the delete process. see 2.3 for more details.
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>
|
||||
|
||||
Return 202 OK
|
||||
|
||||
5. Chaining Registries
|
||||
======================
|
||||
|
||||
It’s possible to chain Registries server for several reasons:
|
||||
- Load balancing
|
||||
- Delegate the next request to another server
|
||||
|
||||
When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download.
|
||||
|
||||
The Index and Registry use this mechanism to redirect on one or the other.
|
||||
|
||||
Example with an image download:
|
||||
On every request, a special header can be returned:
|
||||
|
||||
X-Docker-Endpoints: server1,server2
|
||||
|
||||
On the next request, the client will always pick a server from this list.
|
||||
|
||||
6. Authentication & Authorization
|
||||
=================================
|
||||
|
||||
6.1 On the Index
|
||||
-----------------
|
||||
|
||||
The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Basic realm="auth required",Token
|
||||
|
||||
You have 3 options:
|
||||
|
||||
1. Provide user credentials and ask for a token
|
||||
|
||||
**Header**:
|
||||
- Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
- X-Docker-Token: true
|
||||
|
||||
In this case, along with the 200 response, you’ll get a new token (if user auth is ok):
|
||||
If authorization isn't correct you get a 401 response.
|
||||
If account isn't active you will get a 403 response.
|
||||
|
||||
**Response**:
|
||||
- 200 OK
|
||||
- X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
2. Provide user credentials only
|
||||
|
||||
**Header**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
|
||||
3. Provide Token
|
||||
|
||||
**Header**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
6.2 On the Registry
|
||||
-------------------
|
||||
|
||||
The Registry only supports the Token challenge::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Token
|
||||
|
||||
The only way is to provide a token on “401 Unauthorized” responses::
|
||||
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.::
|
||||
|
||||
200 OK
|
||||
Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
|
||||
|
||||
Next request::
|
||||
|
||||
GET /(...)
|
||||
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="
|
||||
|
||||
|
||||
7.0 Document Version
|
||||
---------------------
|
||||
|
||||
- 1.0 : May 6th 2013 : initial release
|
||||
- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
|
||||
@@ -15,7 +15,7 @@ To list available commands, either run ``docker`` with no parameters or execute
|
||||
|
||||
$ docker
|
||||
Usage: docker [OPTIONS] COMMAND [arg...]
|
||||
-H="127.0.0.1:4243": Host:port to bind/connect to
|
||||
-H=[tcp://127.0.0.1:4243]: tcp://host:port to bind/connect to or unix://path/to/socket to use
|
||||
|
||||
A self-sufficient runtime for linux containers.
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
|
||||
::
|
||||
|
||||
Usage: docker build [OPTIONS] PATH | -
|
||||
Usage: docker build [OPTIONS] PATH | URL | -
|
||||
Build a new container image from the source code at PATH
|
||||
-t="": Tag to be applied to the resulting image in case of success.
|
||||
When a single Dockerfile is given as URL, then no context is set. When a git repository is set as URL, the repository is used as context
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -19,10 +21,23 @@ Examples
|
||||
|
||||
docker build .
|
||||
|
||||
This will take the local Dockerfile
|
||||
| This will read the Dockerfile from the current directory. It will also send any other files and directories found in the current directory to the docker daemon.
|
||||
| The contents of this directory would be used by ADD commands found within the Dockerfile.
|
||||
| This will send a lot of data to the docker daemon if the current directory contains a lot of data.
|
||||
| If the absolute path is provided instead of '.', only the files and directories required by the ADD commands from the Dockerfile will be added to the context and transferred to the docker daemon.
|
||||
|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker build -
|
||||
docker build - < Dockerfile
|
||||
|
||||
This will read a Dockerfile form Stdin without context
|
||||
| This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon.
|
||||
| ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker build github.com/creack/docker-firefox
|
||||
|
||||
| This will clone the github repository and use it as context. The Dockerfile at the root of the repository is used as Dockerfile.
|
||||
| Note that you can specify an arbitrary git repository by using the 'git://' schema.
|
||||
|
||||
@@ -8,6 +8,33 @@
|
||||
|
||||
::
|
||||
|
||||
Usage: docker import [OPTIONS] URL|- [REPOSITORY [TAG]]
|
||||
Usage: docker import URL|- [REPOSITORY [TAG]]
|
||||
|
||||
Create a new filesystem image from the contents of a tarball
|
||||
|
||||
At this time, the URL must start with ``http`` and point to a single file archive (.tar, .tar.gz, .bzip)
|
||||
containing a root filesystem. If you would like to import from a local directory or archive,
|
||||
you can use the ``-`` parameter to take the data from standard in.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Import from a remote location
|
||||
.............................
|
||||
|
||||
``$ docker import http://example.com/exampleimage.tgz exampleimagerepo``
|
||||
|
||||
Import from a local file
|
||||
........................
|
||||
|
||||
Import to docker via pipe and standard in
|
||||
|
||||
``$ cat exampleimage.tgz | docker import - exampleimagelocal``
|
||||
|
||||
Import from a local directory
|
||||
.............................
|
||||
|
||||
``$ sudo tar -c . | docker import - exampleimagedir``
|
||||
|
||||
Note the ``sudo`` in this example -- you must preserve the ownership of the files (especially root ownership)
|
||||
during the archiving with tar. If you are not root (or sudo) when you tar, then the ownerships might not get preserved.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
::
|
||||
|
||||
Usage: docker run [OPTIONS] IMAGE COMMAND [ARG...]
|
||||
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
|
||||
|
||||
Run a command in a new container
|
||||
|
||||
@@ -25,3 +25,5 @@
|
||||
-d=[]: Set custom dns servers for the container
|
||||
-v=[]: Creates a new volume and mounts it at the specified path.
|
||||
-volumes-from="": Mount all volumes from the given container.
|
||||
-b=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]
|
||||
-entrypoint="": Overwrite the default entrypoint set by the image.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
:title: Introduction
|
||||
:description: An introduction to docker and standard containers?
|
||||
:keywords: containers, lxc, concepts, explanation, docker, documentation
|
||||
|
||||
|
||||
:note: This version of the introduction is temporary, just to make sure we don't break the links from the website when the documentation is updated
|
||||
|
||||
This document has been moved to :ref:`introduction`, please update your bookmarks.
|
||||
@@ -1,125 +0,0 @@
|
||||
:title: Introduction
|
||||
:description: An introduction to docker and standard containers?
|
||||
:keywords: containers, lxc, concepts, explanation
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Docker -- The Linux container runtime
|
||||
-------------------------------------
|
||||
|
||||
Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers.
|
||||
|
||||
Docker is a great building block for automating distributed systems: large-scale web deployments, database clusters, continuous deployment systems, private PaaS, service-oriented architectures, etc.
|
||||
|
||||
|
||||
- **Heterogeneous payloads** Any combination of binaries, libraries, configuration files, scripts, virtualenvs, jars, gems, tarballs, you name it. No more juggling between domain-specific tools. Docker can deploy and run them all.
|
||||
- **Any server** Docker can run on any x64 machine with a modern linux kernel - whether it's a laptop, a bare metal server or a VM. This makes it perfect for multi-cloud deployments.
|
||||
- **Isolation** docker isolates processes from each other and from the underlying host, using lightweight containers.
|
||||
- **Repeatability** Because containers are isolated in their own filesystem, they behave the same regardless of where, when, and alongside what they run.
|
||||
|
||||
.. image:: images/lego_docker.jpg
|
||||
|
||||
|
||||
What is a Standard Container?
|
||||
-----------------------------
|
||||
|
||||
Docker defines a unit of software delivery called a Standard Container. The goal of a Standard Container is to encapsulate a software component and all its dependencies in
|
||||
a format that is self-describing and portable, so that any compliant runtime can run it without extra dependency, regardless of the underlying machine and the contents of the container.
|
||||
|
||||
The spec for Standard Containers is currently work in progress, but it is very straightforward. It mostly defines 1) an image format, 2) a set of standard operations, and 3) an execution environment.
|
||||
|
||||
A great analogy for this is the shipping container. Just like Standard Containers are a fundamental unit of software delivery, shipping containers (http://bricks.argz.com/ins/7823-1/12) are a fundamental unit of physical delivery.
|
||||
|
||||
Standard operations
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Just like shipping containers, Standard Containers define a set of STANDARD OPERATIONS. Shipping containers can be lifted, stacked, locked, loaded, unloaded and labelled. Similarly, standard containers can be started, stopped, copied, snapshotted, downloaded, uploaded and tagged.
|
||||
|
||||
|
||||
Content-agnostic
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Just like shipping containers, Standard Containers are CONTENT-AGNOSTIC: all standard operations have the same effect regardless of the contents. A shipping container will be stacked in exactly the same way whether it contains Vietnamese powder coffee or spare Maserati parts. Similarly, Standard Containers are started or uploaded in the same way whether they contain a postgres database, a php application with its dependencies and application server, or Java build artifacts.
|
||||
|
||||
|
||||
Infrastructure-agnostic
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Both types of containers are INFRASTRUCTURE-AGNOSTIC: they can be transported to thousands of facilities around the world, and manipulated by a wide variety of equipment. A shipping container can be packed in a factory in Ukraine, transported by truck to the nearest routing center, stacked onto a train, loaded into a German boat by an Australian-built crane, stored in a warehouse at a US facility, etc. Similarly, a standard container can be bundled on my laptop, uploaded to S3, downloaded, run and snapshotted by a build server at Equinix in Virginia, uploaded to 10 staging servers in a home-made Openstack cluster, then sent to 30 production instances across 3 EC2 regions.
|
||||
|
||||
|
||||
Designed for automation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Because they offer the same standard operations regardless of content and infrastructure, Standard Containers, just like their physical counterpart, are extremely well-suited for automation. In fact, you could say automation is their secret weapon.
|
||||
|
||||
Many things that once required time-consuming and error-prone human effort can now be programmed. Before shipping containers, a bag of powder coffee was hauled, dragged, dropped, rolled and stacked by 10 different people in 10 different locations by the time it reached its destination. 1 out of 50 disappeared. 1 out of 20 was damaged. The process was slow, inefficient and cost a fortune - and was entirely different depending on the facility and the type of goods.
|
||||
|
||||
Similarly, before Standard Containers, by the time a software component ran in production, it had been individually built, configured, bundled, documented, patched, vendored, templated, tweaked and instrumented by 10 different people on 10 different computers. Builds failed, libraries conflicted, mirrors crashed, post-it notes were lost, logs were misplaced, cluster updates were half-broken. The process was slow, inefficient and cost a fortune - and was entirely different depending on the language and infrastructure provider.
|
||||
|
||||
|
||||
Industrial-grade delivery
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are 17 million shipping containers in existence, packed with every physical good imaginable. Every single one of them can be loaded on the same boats, by the same cranes, in the same facilities, and sent anywhere in the World with incredible efficiency. It is embarrassing to think that a 30 ton shipment of coffee can safely travel half-way across the World in *less time* than it takes a software team to deliver its code from one datacenter to another sitting 10 miles away.
|
||||
|
||||
With Standard Containers we can put an end to that embarrassment, by making INDUSTRIAL-GRADE DELIVERY of software a reality.
|
||||
|
||||
|
||||
Standard Container Specification
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
(TODO)
|
||||
|
||||
Image format
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Standard operations
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Copy
|
||||
- Run
|
||||
- Stop
|
||||
- Wait
|
||||
- Commit
|
||||
- Attach standard streams
|
||||
- List filesystem changes
|
||||
- ...
|
||||
|
||||
Execution environment
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Root filesystem
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Environment variables
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Process arguments
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Networking
|
||||
^^^^^^^^^^
|
||||
|
||||
Process namespacing
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Resource limits
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Process monitoring
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Logging
|
||||
^^^^^^^
|
||||
|
||||
Signals
|
||||
^^^^^^^
|
||||
|
||||
Pseudo-terminal allocation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
@@ -20,6 +20,21 @@ import sys, os
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# the 'redirect_home.html' page redirects using a http meta refresh which, according
|
||||
# to official sources is more or less equivalent of a 301.
|
||||
|
||||
html_additional_pages = {
|
||||
'concepts/containers': 'redirect_home.html',
|
||||
'concepts/introduction': 'redirect_home.html',
|
||||
'builder/basics': 'redirect_build.html',
|
||||
}
|
||||
|
||||
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
@@ -120,7 +135,11 @@ html_theme_path = ['../theme']
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# We use a png favicon. This is not compatible with internet explorer, but looks
|
||||
# much better on all other browsers. However, sphynx doesn't like it (it likes
|
||||
# .ico better) so we have just put it in the template rather than used this setting
|
||||
# html_favicon = 'favicon.png'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -138,10 +157,6 @@ html_static_path = ['static_files']
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
Contributing to Docker
|
||||
======================
|
||||
|
||||
Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started <https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md>`.
|
||||
Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started <https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md>`_.
|
||||
|
||||
|
||||
@@ -5,11 +5,35 @@
|
||||
Setting Up a Dev Environment
|
||||
============================
|
||||
|
||||
Instructions that have been verified to work on Ubuntu 12.10,
|
||||
Instructions that have been verified to work on Ubuntu Precise 12.04 (LTS) (64-bit),
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
**Linux kernel 3.8**
|
||||
|
||||
Due to a bug in LXC docker works best on the 3.8 kernel. Precise comes with a 3.2 kernel, so we need to upgrade it. The kernel we install comes with AUFS built in.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get -y install lxc wget bsdtar curl golang git
|
||||
# install the backported kernel
|
||||
sudo apt-get update && sudo apt-get install linux-image-generic-lts-raring
|
||||
|
||||
# reboot
|
||||
sudo reboot
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:gophers/go
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install lxc xz-utils curl golang-stable git aufs-tools
|
||||
|
||||
export GOPATH=~/go/
|
||||
export PATH=$GOPATH/bin:$PATH
|
||||
|
||||
@@ -72,7 +72,7 @@ Connect to the host os with the redis-cli.
|
||||
|
||||
docker ps # grab the new container id
|
||||
docker port <container_id> 6379 # grab the external port
|
||||
ifconfig # grab the host ip address
|
||||
ip addr show # grab the host ip address
|
||||
redis-cli -h <host ipaddress> -p <external port>
|
||||
redis 192.168.0.1:49153> set docker awesome
|
||||
OK
|
||||
|
||||
@@ -59,6 +59,7 @@ The password is 'screencast'
|
||||
# it has now given us a port to connect to
|
||||
# we have to connect using a public ip of our host
|
||||
$ hostname
|
||||
# *ifconfig* is deprecated, better use *ip addr show* now
|
||||
$ ifconfig
|
||||
$ ssh root@192.168.33.10 -p 49153
|
||||
# Ah! forgot to set root passwd
|
||||
@@ -70,6 +71,7 @@ The password is 'screencast'
|
||||
$ docker commit 9e863f0ca0af31c8b951048ba87641d67c382d08d655c2e4879c51410e0fedc1 dhrp/sshd
|
||||
$ docker run -d -p 22 dhrp/sshd /usr/sbin/sshd -D
|
||||
$ docker port a0aaa9558c90cf5c7782648df904a82365ebacce523e4acc085ac1213bfe2206 22
|
||||
# *ifconfig* is deprecated, better use *ip addr show* now
|
||||
$ ifconfig
|
||||
$ ssh root@192.168.33.10 -p 49154
|
||||
# Thanks for watching, Thatcher thatcher@dotcloud.com
|
||||
|
||||
@@ -19,7 +19,8 @@ Most frequently asked questions.
|
||||
|
||||
3. **Does Docker run on Mac OS X or Windows?**
|
||||
|
||||
Not at this time, Docker currently only runs on Linux, but you can use VirtualBox to run Docker in a virtual machine on your box, and get the best of both worlds. Check out the MacOSX_ and Windows_ installation guides.
|
||||
Not at this time, Docker currently only runs on Linux, but you can use VirtualBox to run Docker in a
|
||||
virtual machine on your box, and get the best of both worlds. Check out the :ref:`install_using_vagrant` and :ref:`windows` installation guides.
|
||||
|
||||
4. **How do containers compare to virtual machines?**
|
||||
|
||||
@@ -34,15 +35,16 @@ Most frequently asked questions.
|
||||
|
||||
You can find more answers on:
|
||||
|
||||
* `IRC: docker on freenode`_
|
||||
* `Docker club mailinglist`_
|
||||
* `IRC, docker on freenode`_
|
||||
* `Github`_
|
||||
* `Ask questions on Stackoverflow`_
|
||||
* `Join the conversation on Twitter`_
|
||||
|
||||
.. _Windows: ../installation/windows/
|
||||
.. _MacOSX: ../installation/vagrant/
|
||||
|
||||
.. _Docker club mailinglist: https://groups.google.com/d/forum/docker-club
|
||||
.. _the repo: http://www.github.com/dotcloud/docker
|
||||
.. _IRC\: docker on freenode: irc://chat.freenode.net#docker
|
||||
.. _IRC, docker on freenode: irc://chat.freenode.net#docker
|
||||
.. _Github: http://www.github.com/dotcloud/docker
|
||||
.. _Ask questions on Stackoverflow: http://stackoverflow.com/search?q=docker
|
||||
.. _Join the conversation on Twitter: http://twitter.com/getdocker
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
:title: Index Environment Variable
|
||||
:description: Setting this environment variable on the docker server will change the URL docker index.
|
||||
:keywords: docker, index environment variable, documentation
|
||||
|
||||
=================================
|
||||
Docker Index Environment Variable
|
||||
=================================
|
||||
|
||||
Variable
|
||||
--------
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
DOCKER_INDEX_URL
|
||||
|
||||
Setting this environment variable on the docker server will change the URL docker index.
|
||||
This address is used in commands such as ``docker login``, ``docker push`` and ``docker pull``.
|
||||
The docker daemon doesn't need to be restarted for this parameter to take effect.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
docker -d &
|
||||
export DOCKER_INDEX_URL="https://index.docker.io"
|
||||
|
||||
@@ -30,8 +30,7 @@ Dependencies:
|
||||
* 3.8 Kernel (read more about :ref:`kernel`)
|
||||
* AUFS filesystem support
|
||||
* lxc
|
||||
* bsdtar
|
||||
|
||||
* xz-utils
|
||||
|
||||
Get the docker binary:
|
||||
----------------------
|
||||
|
||||
@@ -100,7 +100,7 @@ Memory and Swap Accounting on Debian/Ubuntu
|
||||
If you use Debian or Ubuntu kernels, and want to enable memory and swap
|
||||
accounting, you must add the following command-line parameters to your kernel::
|
||||
|
||||
cgroup_enable=memory swapaccount
|
||||
cgroup_enable=memory swapaccount=1
|
||||
|
||||
On Debian or Ubuntu systems, if you use the default GRUB bootloader, you can
|
||||
add those parameters by editing ``/etc/default/grub`` and extending
|
||||
@@ -110,6 +110,6 @@ add those parameters by editing ``/etc/default/grub`` and extending
|
||||
|
||||
And replace it by the following one::
|
||||
|
||||
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount"
|
||||
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
|
||||
|
||||
Then run ``update-grub``, and reboot.
|
||||
|
||||
@@ -92,6 +92,16 @@ have AUFS filesystem support enabled, so we need to install it.
|
||||
sudo apt-get update
|
||||
sudo apt-get install linux-image-extra-`uname -r`
|
||||
|
||||
**add-apt-repository support**
|
||||
|
||||
Some installations of Ubuntu 13.04 require ``software-properties-common`` to be
|
||||
installed before being able to use add-apt-repository.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install software-properties-common
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
:description: Docker's tutorial to run docker on Windows
|
||||
:keywords: Docker, Docker documentation, Windows, requirements, virtualbox, vagrant, git, ssh, putty, cygwin
|
||||
|
||||
.. _windows:
|
||||
|
||||
Using Vagrant (Windows)
|
||||
=======================
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
:title: Image & Container
|
||||
:description: Definitions of an image and container
|
||||
:keywords: containers, lxc, concepts, explanation, image, container
|
||||
|
||||
File Systems
|
||||
============
|
||||
|
||||
.. image:: images/docker-filesystems-generic.png
|
||||
|
||||
In order for a Linux system to run, it typically needs two `file
|
||||
systems <http://en.wikipedia.org/wiki/Filesystem>`_:
|
||||
|
||||
1. boot file system (bootfs)
|
||||
2. root file system (rootfs)
|
||||
|
||||
The **boot file system** contains the bootloader and the kernel. The
|
||||
user never makes any changes to the boot file system. In fact, soon
|
||||
after the boot process is complete, the entire kernel is in memory,
|
||||
and the boot file system is unmounted to free up the RAM associated
|
||||
with the initrd disk image.
|
||||
|
||||
The **root file system** includes the typical directory structure we
|
||||
associate with Unix-like operating systems: ``/dev, /proc, /bin, /etc,
|
||||
/lib, /usr,`` and ``/tmp`` plus all the configuration files, binaries
|
||||
and libraries required to run user applications (like bash, ls, and so
|
||||
forth).
|
||||
|
||||
While there can be important kernal differences between different
|
||||
Linux distributions, the contents and organization of the root file
|
||||
system are usually what make your software packages dependent on one
|
||||
distribution versus another. Docker can help solve this problem by
|
||||
running multiple distributions at the same time.
|
||||
|
||||
.. image:: images/docker-filesystems-multiroot.png
|
||||
|
||||
Layers and Union Mounts
|
||||
=======================
|
||||
|
||||
In a traditional Linux boot, the kernel first mounts the root file
|
||||
system as read-only, checks its integrity, and then switches the whole
|
||||
rootfs volume to read-write mode. Docker does something similar,
|
||||
*except* that instead of changing the file system to read-write mode,
|
||||
it takes advantage of a `union mount
|
||||
<http://en.wikipedia.org/wiki/Union_mount>`_ to add a read-write file
|
||||
system *over* the read-only file system. In fact there may be multiple
|
||||
read-only file systems stacked on top of each other.
|
||||
|
||||
.. image:: images/docker-filesystems-multilayer.png
|
||||
|
||||
At first, the top layer has nothing in it, but any time a process
|
||||
creates a file, this happens in the top layer. And if something needs
|
||||
to update an existing file in a lower layer, then the file gets copied
|
||||
to the upper layer and changes go into the copy. The version of the
|
||||
file on the lower layer cannot be seen by the applications anymore,
|
||||
but it is there, unchanged.
|
||||
|
||||
We call the union of the read-write layer and all the read-only layers
|
||||
a **union file system**.
|
||||
|
||||
Image
|
||||
=====
|
||||
|
||||
In Docker terminology, a read-only layer is called an **image**. An
|
||||
image never changes. Because Docker uses a union file system, the
|
||||
applications think the whole file system is mounted read-write,
|
||||
because any file can be changed. But all the changes go to the
|
||||
top-most layer, and underneath, the image is unchanged. Since they
|
||||
don't change, images do not have state.
|
||||
|
||||
Each image may depend on one more image which forms the layer beneath
|
||||
it. We sometimes say that the lower image is the **parent** of the
|
||||
upper image.
|
||||
|
||||
Base Image
|
||||
==========
|
||||
|
||||
An image that has no parent is a **base image**.
|
||||
|
||||
Container
|
||||
=========
|
||||
|
||||
Once you start a process in Docker from an image, Docker fetches the
|
||||
image and its parent, and repeats the process until it reaches the
|
||||
base image. Then the union file system adds a read-write layer on
|
||||
top. That read-write layer, plus the information about its parent and
|
||||
some additional information like its unique id, is called a
|
||||
**container**.
|
||||
|
||||
Containers can change, and so they have state. A container may be
|
||||
running or exited. In either case, the state of the file system and
|
||||
its exit value is preserved. You can start, stop, and restart a
|
||||
container. The processes restart from scratch (their memory state is
|
||||
**not** preserved in a container), but the file system is just as it
|
||||
was when the container was stopped.
|
||||
|
||||
You can promote a container to an image with ``docker commit``. Once a
|
||||
container is an image, you can use it as a parent for new containers.
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 243 KiB |
@@ -0,0 +1,18 @@
|
||||
:title: Terms
|
||||
:description: Definitions of terms used in Docker documentation
|
||||
:keywords: concepts, documentation, docker, containers
|
||||
|
||||
|
||||
|
||||
Terms
|
||||
=====
|
||||
|
||||
Definitions of terms used in Docker documentation.
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
fundamentals
|
||||
|
||||
@@ -18,5 +18,6 @@ This documentation has the following resources:
|
||||
contributing/index
|
||||
api/index
|
||||
faq
|
||||
terms/index
|
||||
|
||||
.. image:: concepts/images/lego_docker.jpg
|
||||
|
||||
@@ -33,11 +33,20 @@ Running an interactive shell
|
||||
# allocate a tty, attach stdin and stdout
|
||||
docker run -i -t base /bin/bash
|
||||
|
||||
Bind Docker to another host/port
|
||||
--------------------------------
|
||||
Bind Docker to another host/port or a unix socket
|
||||
-------------------------------------------------
|
||||
|
||||
If you want Docker to listen to another port and bind to another ip
|
||||
use -host and -port on both deamon and client
|
||||
With -H it is possible to make the Docker daemon to listen on a specific ip and port. By default, it will listen on 127.0.0.1:4243 to allow only local connections but you can set it to 0.0.0.0:4243 or a specific host ip to give access to everybody.
|
||||
|
||||
Similarly, the Docker client can use -H to connect to a custom port.
|
||||
|
||||
-H accepts host and port assignment in the following format: tcp://[host][:port] or unix://path
|
||||
For example:
|
||||
|
||||
* tcp://host -> tcp connection on host:4243
|
||||
* tcp://host:port -> tcp connection on host:port
|
||||
* tcp://:port -> tcp connection on 127.0.0.1:port
|
||||
* unix://path/to/socket -> unix socket located at path/to/socket
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -46,6 +55,17 @@ use -host and -port on both deamon and client
|
||||
# Download a base image
|
||||
docker -H :5555 pull base
|
||||
|
||||
You can use multiple -H, for example, if you want to listen
|
||||
on both tcp and a unix socket
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run docker in daemon mode
|
||||
sudo <path to>/docker -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock
|
||||
# Download a base image
|
||||
docker pull base
|
||||
# OR
|
||||
docker -H unix:///var/run/docker.sock pull base
|
||||
|
||||
Starting a long-running worker process
|
||||
--------------------------------------
|
||||
@@ -82,7 +102,8 @@ Expose a service on a TCP port
|
||||
|
||||
# Connect to the public port via the host's public address
|
||||
# Please note that because of how routing works connecting to localhost or 127.0.0.1 $PORT will not work.
|
||||
IP=$(ifconfig eth0 | perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }')
|
||||
# Replace *eth0* according to your local interface name.
|
||||
IP=$(ip -o -4 addr list eth0 | perl -n -e 'if (m{inet\s([\d\.]+)\/\d+\s}xms) { print $1 }')
|
||||
echo hello world | nc $IP $PORT
|
||||
|
||||
# Verify that the network connection worked
|
||||
|
||||
@@ -15,10 +15,18 @@ steps and commit them along the way, giving you a final image.
|
||||
1. Usage
|
||||
========
|
||||
|
||||
To use Docker Builder, assemble the steps into a text file (commonly referred to
|
||||
as a Dockerfile) and supply this to `docker build` on STDIN, like so:
|
||||
To build an image from a source repository, create a description file called `Dockerfile`
|
||||
at the root of your repository. This file will describe the steps to assemble
|
||||
the image.
|
||||
|
||||
``docker build < Dockerfile``
|
||||
Then call `docker build` with the path of your source repository as argument:
|
||||
|
||||
``docker build .``
|
||||
|
||||
You can specify a repository and tag at which to save the new image if the
|
||||
build succeeds:
|
||||
|
||||
``docker build -t shykes/myapp .``
|
||||
|
||||
Docker will run your steps one-by-one, committing the result if necessary,
|
||||
before finally outputting the ID of your new image.
|
||||
@@ -113,26 +121,44 @@ functionally equivalent to prefixing the command with `<key>=<value>`
|
||||
.. note::
|
||||
The environment variables will persist when a container is run from the resulting image.
|
||||
|
||||
2.7 INSERT
|
||||
----------
|
||||
|
||||
``INSERT <file url> <path>``
|
||||
|
||||
The `INSERT` instruction will download the file from the given url to the given
|
||||
path within the image. It is similar to `RUN curl -o <path> <url>`, assuming
|
||||
curl was installed within the image.
|
||||
|
||||
.. note::
|
||||
The path must include the file name.
|
||||
|
||||
2.8 ADD
|
||||
2.7 ADD
|
||||
-------
|
||||
|
||||
``ADD <src> <dest>``
|
||||
|
||||
The `ADD` instruction will insert the files from the `<src>` path of the context into `<dest>` path
|
||||
of the container.
|
||||
The context must be set in order to use this instruction. (see examples)
|
||||
The `ADD` instruction will copy new files from <src> and add them to the container's filesystem at path `<dest>`.
|
||||
|
||||
`<src>` must be the path to a file or directory relative to the source directory being built (also called the
|
||||
context of the build) or a remote file URL.
|
||||
|
||||
`<dest>` is the path at which the source will be copied in the destination container.
|
||||
|
||||
The copy obeys the following rules:
|
||||
|
||||
If `<src>` is a directory, the entire directory is copied, including filesystem metadata.
|
||||
|
||||
If `<src>` is a tar archive in a recognized compression format (identity, gzip, bzip2 or xz), it
|
||||
is unpacked as a directory.
|
||||
|
||||
When a directory is copied or unpacked, it has the same behavior as 'tar -x': the result is the union of
|
||||
a) whatever existed at the destination path and b) the contents of the source tree, with conflicts resolved
|
||||
in favor of b on a file-by-file basis.
|
||||
|
||||
If `<src>` is any other kind of file, it is copied individually along with its metadata. In this case,
|
||||
if `<dst>` ends with a trailing slash '/', it will be considered a directory and the contents of `<src>`
|
||||
will be written at `<dst>/base(<src>)`.
|
||||
If `<dst>` does not end with a trailing slash, it will be considered a regular file and the contents
|
||||
of `<src>` will be written at `<dst>`.
|
||||
|
||||
If `<dest>` doesn't exist, it is created along with all missing directories in its path. All new
|
||||
files and directories are created with mode 0700, uid and gid 0.
|
||||
|
||||
2.8 ENTRYPOINT
|
||||
-------------
|
||||
|
||||
``ENTRYPOINT /bin/echo``
|
||||
|
||||
The `ENTRYPOINT` instruction adds an entry command that will not be overwritten when arguments are passed to docker run, unlike the behavior of `CMD`. This allows arguments to be passed to the entrypoint. i.e. `docker run <image> -d` will pass the "-d" argument to the entrypoint.
|
||||
|
||||
3. Dockerfile Examples
|
||||
======================
|
||||
@@ -151,7 +177,6 @@ The context must be set in order to use this instruction. (see examples)
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y inotify-tools nginx apache2 openssh-server
|
||||
INSERT https://raw.github.com/creack/docker-vps/master/nginx-wrapper.sh /usr/sbin/nginx-wrapper
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Contents:
|
||||
|
||||
basics
|
||||
workingwithrepository
|
||||
port_redirection
|
||||
builder
|
||||
puppet
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
:title: Port redirection
|
||||
:description: usage about port redirection
|
||||
:keywords: Usage, basic port, docker, documentation, examples
|
||||
|
||||
|
||||
Port redirection
|
||||
================
|
||||
|
||||
Docker can redirect public tcp ports to your container, so it can be reached over the network.
|
||||
Port redirection is done on ``docker run`` using the -p flag.
|
||||
|
||||
A port redirect is specified as PUBLIC:PRIVATE, where tcp port PUBLIC will be redirected to
|
||||
tcp port PRIVATE. As a special case, the public port can be omitted, in which case a random
|
||||
public port will be allocated.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# A random PUBLIC port is redirected to PRIVATE port 80 on the container
|
||||
docker run -p 80 <image> <cmd>
|
||||
|
||||
# PUBLIC port 80 is redirected to PRIVATE port 80
|
||||
docker run -p 80:80 <image> <cmd>
|
||||
|
||||
|
||||
Default port redirects can be built into a container with the EXPOSE build command.
|
||||
@@ -77,3 +77,28 @@ Now you can commit this image to the repository
|
||||
# for example docker push dhrp/kickassapp
|
||||
docker push <image-name>
|
||||
|
||||
|
||||
Changing the server to connect to
|
||||
----------------------------------
|
||||
|
||||
When you are running your own index and/or registry, You can change the server the docker client will connect to.
|
||||
|
||||
Variable
|
||||
^^^^^^^^
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
DOCKER_INDEX_URL
|
||||
|
||||
Setting this environment variable on the docker server will change the URL docker index.
|
||||
This address is used in commands such as ``docker login``, ``docker push`` and ``docker pull``.
|
||||
The docker daemon doesn't need to be restarted for this parameter to take effect.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
docker -d &
|
||||
export DOCKER_INDEX_URL="https://index.docker.io"
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
|
||||
{%- set script_files = script_files + ['_static/js/docs.js'] %}
|
||||
|
||||
{%- if pagename == 'index' %}
|
||||
<link rel="canonical" href="http://docs.docker.io/en/latest/">
|
||||
{% else %}
|
||||
<link rel="canonical" href="http://docs.docker.io/en/latest/{{ pagename }}/">
|
||||
{% endif %}
|
||||
{%- for cssfile in css_files %}
|
||||
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
|
||||
{%- endfor %}
|
||||
@@ -48,9 +53,8 @@
|
||||
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
|
||||
{%- endfor %}
|
||||
|
||||
{%- if favicon %}
|
||||
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
|
||||
{%- endif %}
|
||||
<link rel="shortcut icon" href="{{ pathto('_static/favicon.png', 1) }}"/>
|
||||
|
||||
|
||||
{%- block extrahead %}{% endblock %}
|
||||
|
||||
@@ -64,14 +68,15 @@
|
||||
|
||||
<div style="float: right" class="pull-right">
|
||||
<ul class="nav">
|
||||
<li><a href="http://www.docker.io/">Introduction</a></li>
|
||||
<li><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
|
||||
<li class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-introduction"><a href="http://www.docker.io/">Introduction</a></li>
|
||||
<li id="nav-gettingstarted"><a href="http://www.docker.io/gettingstarted/">Getting started</a></li>
|
||||
<li id="nav-documentation" class="active"><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
|
||||
</ul>
|
||||
<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
<!--<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">-->
|
||||
<!--<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>-->
|
||||
<!--<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>-->
|
||||
<!--</div>-->
|
||||
</div>
|
||||
|
||||
<div style="margin-left: -12px; float: left;">
|
||||
@@ -86,8 +91,13 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="span12 titlebar"><h1 class="pageheader">DOCUMENTATION</h1>
|
||||
<div class="span12 titlebar">
|
||||
<!--<span class="pull-right" style="margin-left: 20px; font-size: 20px">{{version}}</span>-->
|
||||
<div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
|
||||
<a href="http://github.com/dotcloud/docker/"><img src="{{ pathto('_static/img/fork-us.png', 1) }}"> Fork us on Github</a>
|
||||
</div>
|
||||
<h1 class="pageheader">DOCUMENTATION</h1>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,11 +108,8 @@
|
||||
<!-- Docs nav
|
||||
================================================== -->
|
||||
<div class="row" style="position: relative">
|
||||
<div class="span3" style="height:100%;" >
|
||||
|
||||
</div>
|
||||
|
||||
<div class="span3 sidebar bs-docs-sidebar" style="position: absolute">
|
||||
<div class="span3 sidebar bs-docs-sidebar">
|
||||
{{ toctree(collapse=False, maxdepth=3) }}
|
||||
</div>
|
||||
|
||||
@@ -123,8 +130,14 @@
|
||||
<div class="row">
|
||||
|
||||
<div class="span12 footer">
|
||||
<div class="tbox textright forceleftmargin social links pull-right">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
|
||||
Docker is a project by <a href="http://www.dotcloud.com">dotCloud</a>
|
||||
|
||||
|
||||
{# {%- if show_source and has_source and sourcename %}#}
|
||||
{# ·#}
|
||||
{# <a href="{{ pathto('_sources/' + sourcename, true)|e }}"#}
|
||||
@@ -157,7 +170,7 @@
|
||||
<!-- script which should be loaded after everything else -->
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
// Function to make the sticky header possible
|
||||
var shiftWindow = function() {
|
||||
scrollBy(0, -70);
|
||||
console.log("window shifted")
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page Moved</title>
|
||||
<meta http-equiv="refresh" content="0; url=http://docs.docker.io/en/latest/use/builder/">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
This page has moved. Perhaps you should visit the <a href="http://docs.docker.io/en/latest/use/builder/" title="builder page">Builder page</a>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page Moved</title>
|
||||
<meta http-equiv="refresh" content="0; url=http://docs.docker.io/en/latest/">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
This page has moved. Perhaps you should visit the <a href="http://docs.docker.io/" title="documentation homepage">Documentation Homepage</a>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -168,10 +168,13 @@ section.header {
|
||||
.sidebar {
|
||||
font-weight: normal;
|
||||
float: left;
|
||||
min-height: 475px;
|
||||
/* min-height: 475px;*/
|
||||
|
||||
background: #ececec;
|
||||
border-left: 1px solid #bbbbbb;
|
||||
border-right: 1px solid #cccccc;
|
||||
/* border-left: 1px solid #bbbbbb;*/
|
||||
|
||||
/* border-right: 1px solid #cccccc;*/
|
||||
|
||||
position: relative;
|
||||
}
|
||||
.sidebar ul {
|
||||
@@ -285,6 +288,40 @@ section.header {
|
||||
.social .github {
|
||||
background-position: -59px 2px;
|
||||
}
|
||||
#fork-us {
|
||||
/*font-family: 'Maven Pro';*/
|
||||
|
||||
/*font-weight: bold;*/
|
||||
|
||||
font-size: 12px;
|
||||
/*text-transform: uppercase;*/
|
||||
|
||||
display: block;
|
||||
padding: 0px 1em;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
background-color: #43484c;
|
||||
filter: progid:dximagetransform.microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
|
||||
background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: -o-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
border: 1px solid #43484c;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
-ms-border-radius: 4px;
|
||||
-o-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
-moz-box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
margin: 8px;
|
||||
}
|
||||
#fork-us a {
|
||||
color: #faf2ee;
|
||||
text-shadow: rgba(0, 0, 0, 0.3) 0px 1px 0px;
|
||||
}
|
||||
/* =======================
|
||||
Media size overrides
|
||||
======================= */
|
||||
@@ -323,12 +360,16 @@ section.header {
|
||||
#global {
|
||||
/* TODO: Fix this to be relative to the navigation size */
|
||||
|
||||
padding-top: 600px;
|
||||
}
|
||||
#fork-us {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Landscape phones and down */
|
||||
@media (max-width: 480px) {
|
||||
|
||||
#nav-gettingstarted {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Misc fixes */
|
||||
table th {
|
||||
|
||||
@@ -226,20 +226,21 @@ section.header {
|
||||
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
// font-family: "Maven Pro";
|
||||
font-weight: normal;
|
||||
// margin-top: 38px;
|
||||
float: left;
|
||||
// width: 220px;
|
||||
/* min-height: 475px;*/
|
||||
// margin-bottom: 28px;
|
||||
// padding-bottom: 120px;
|
||||
background: #ececec;
|
||||
/* border-left: 1px solid #bbbbbb;*/
|
||||
/* border-right: 1px solid #cccccc;*/
|
||||
position: relative;
|
||||
|
||||
|
||||
.sidebar {
|
||||
// font-family: "Maven Pro";
|
||||
font-weight: normal;
|
||||
// margin-top: 38px;
|
||||
float: left;
|
||||
// width: 220px;
|
||||
min-height: 475px;
|
||||
// margin-bottom: 28px;
|
||||
// padding-bottom: 120px;
|
||||
background: #ececec;
|
||||
border-left: 1px solid #bbbbbb;
|
||||
border-right: 1px solid #cccccc;
|
||||
position: relative;
|
||||
|
||||
ul {
|
||||
padding: 0px;
|
||||
@@ -391,6 +392,38 @@ section.header {
|
||||
}
|
||||
|
||||
|
||||
#fork-us {
|
||||
/*font-family: 'Maven Pro';*/
|
||||
/*font-weight: bold;*/
|
||||
font-size: 12px;
|
||||
/*text-transform: uppercase;*/
|
||||
display: block;
|
||||
padding: 0px 1em;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
background-color: #43484c;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFFF6E56', endColorstr='#FFED4F35');
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #747474), color-stop(100%, #43484c));
|
||||
background-image: -webkit-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: -moz-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: -o-linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
background-image: linear-gradient(top, #747474 0%, #43484c 100%);
|
||||
border: 1px solid #43484c;
|
||||
-webkit-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
-ms-border-radius: 4px;
|
||||
-o-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
-moz-box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
box-shadow: inset rgba(255, 255, 255, 0.17) 0 1px 1px;
|
||||
margin: 8px;
|
||||
|
||||
a {
|
||||
color: #faf2ee;
|
||||
text-shadow: rgba(0, 0, 0, 0.3) 0px 1px 0px;
|
||||
}
|
||||
}
|
||||
/* =======================
|
||||
Media size overrides
|
||||
======================= */
|
||||
@@ -439,16 +472,19 @@ section.header {
|
||||
}
|
||||
#global {
|
||||
/* TODO: Fix this to be relative to the navigation size */
|
||||
padding-top: 600px;
|
||||
// padding-top: 600px;
|
||||
}
|
||||
#fork-us {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Landscape phones and down */
|
||||
@media (max-width: 480px) {
|
||||
|
||||
|
||||
#nav-gettingstarted {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Misc fixes */
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 404 B |
@@ -34,15 +34,11 @@
|
||||
|
||||
<div style="float: right" class="pull-right">
|
||||
<ul class="nav">
|
||||
<li><a href="../">Introduction</a></li>
|
||||
<li class="active"><a href="">Getting started</a></li>
|
||||
<li class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-introduction"><a href="../">Introduction</a></li>
|
||||
<li id="nav-gettingstarted" class="active"><a href="">Getting started</a></li>
|
||||
<li id="nav-documentation" class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: -12px; float: left;">
|
||||
@@ -55,14 +51,22 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="span12 titlebar"><h1 class="pageheader">GETTING STARTED</h1>
|
||||
|
||||
<div class="span12 titlebar">
|
||||
|
||||
<div class="pull-right" id="fork-us" style="margin-top: 16px; margin-right: 16px;">
|
||||
<a href="http://github.com/dotcloud/docker/"><img src="../static/img/fork-us.png"> Fork us on Github</a>
|
||||
</div>
|
||||
|
||||
<h1 class="pageheader"> GETTING STARTED</h1>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="alert alert-info">
|
||||
<div class="alert alert-info" style="margin-bottom: 0;">
|
||||
<strong>Docker is still under heavy development.</strong> It should not yet be used in production. Check <a href="http://github.com/dotcloud/docker">the repo</a> for recent progress.
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -133,13 +137,13 @@
|
||||
</section>
|
||||
|
||||
<section class="contentblock">
|
||||
<h2>More resources</h2>
|
||||
<ul>
|
||||
<li><a href="irc://chat.freenode.net#docker">IRC: docker on freenode</a></li>
|
||||
<li><a href="http://www.github.com/dotcloud/docker">Github</a></li>
|
||||
<li><a href="http://stackoverflow.com/tags/docker/">Ask questions on Stackoverflow</a></li>
|
||||
<li><a href="http://twitter.com/getdocker/">Join the conversation on Twitter</a></li>
|
||||
</ul>
|
||||
<h2>Questions? Want to get in touch?</h2>
|
||||
<p>There are several ways to get in touch:</p>
|
||||
<p><strong>Join the discussion on IRC.</strong> We can be found in the <a href="irc://chat.freenode.net#docker">#docker</a> channel on chat.freenode.net</p>
|
||||
<p><strong>Discussions</strong> happen on our google group: <a href="https://groups.google.com/d/forum/docker-club">docker-club at googlegroups.com</a></p>
|
||||
<p>All our <strong>development and decisions</strong> are made out in the open on Github <a href="http://www.github.com/dotcloud/docker">github.com/dotcloud/docker</a></p>
|
||||
<p><strong>Get help on using Docker</strong> by asking on <a href="http://stackoverflow.com/tags/docker/">Stackoverflow</a></p>
|
||||
<p>And of course, <strong>tweet</strong> your tweets to <a href="http://twitter.com/getdocker/">twitter.com/getdocker</a></p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -172,7 +176,10 @@
|
||||
<footer id="footer" class="footer">
|
||||
<div class="row">
|
||||
<div class="span12 social">
|
||||
|
||||
<div class="tbox textright forceleftmargin social links pull-right">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
Docker is a project by <a href="http://www.dotcloud.com">dotCloud</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,18 @@
|
||||
.debug {
|
||||
border: 1px red dotted;
|
||||
}
|
||||
.twitterblock {
|
||||
min-height: 75px;
|
||||
}
|
||||
|
||||
.twitterblock img {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
@@ -56,17 +65,18 @@
|
||||
<div class="navbar-dotcloud">
|
||||
<div class="container" style="text-align: center;">
|
||||
|
||||
|
||||
<div class="pull-left" id="fork-us" style="margin-top: 16px;">
|
||||
<a href="http://github.com/dotcloud/docker/"><img src="static/img/fork-us.png" alt="fork-icon"> Fork us on Github</a>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" >
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="/">Introduction</a></li>
|
||||
<li ><a href="gettingstarted">Getting started</a></li>
|
||||
<li class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-introduction" class="active"><a href="/">Introduction</a></li>
|
||||
<li id="nav-gettingstarted"><a href="gettingstarted">Getting started</a></li>
|
||||
<li id="nav-documentation" class=""><a href="http://docs.docker.io/en/latest/">Documentation</a></li>
|
||||
<li id="nav-blog"><a href="http://blog.docker.io/">Blog</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="social links" style="float: right; margin-top: 14px; margin-left: 12px">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +91,7 @@
|
||||
|
||||
<div class="span5" style="margin-bottom: 15px;">
|
||||
<div style="text-align: center;" >
|
||||
<img src="static/img/docker_letters_500px.png">
|
||||
<img src="static/img/docker_letters_500px.png" alt="docker letters">
|
||||
|
||||
<h2>The Linux container engine</h2>
|
||||
</div>
|
||||
@@ -130,7 +140,7 @@
|
||||
<section class="contentblock">
|
||||
<div class="container">
|
||||
<div class="span2" style="margin-left: 0" >
|
||||
<a href="http://dotcloud.theresumator.com/apply/mWjkD4/Software-Engineer.html" title="Job description"><img src="static/img/hiring_graphic.png" width="140px" style="margin-top: 25px"></a>
|
||||
<a href="http://dotcloud.theresumator.com/apply/mWjkD4/Software-Engineer.html" title="Job description"><img src="static/img/hiring_graphic.png" alt="we're hiring" width="140" style="margin-top: 25px"></a>
|
||||
</div>
|
||||
<div class="span4" style="margin-left: 0">
|
||||
<h4>Do you think it is cool to hack on docker? Join us!</h4>
|
||||
@@ -156,7 +166,7 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<input type="button" class="searchbutton" type="submit" value="Search images"
|
||||
<input type="button" class="searchbutton" value="Search images"
|
||||
onClick="window.open('https://index.docker.io')" />
|
||||
|
||||
</section>
|
||||
@@ -184,32 +194,19 @@
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.twitterblock {
|
||||
min-height: 75px;
|
||||
}
|
||||
|
||||
.twitterblock img {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
<section class="contentblock twitterblock">
|
||||
<img src="https://si0.twimg.com/profile_images/2707460527/252a64411a339184ff375a96fb68dcb0_bigger.png">
|
||||
<em>Mitchell Hashimoto@mitchellh:</em> Docker launched today. It is incredible. They’re also working RIGHT NOW on a Vagrant provider. LXC is COMING!!
|
||||
<em>Mitchell Hashimoto @mitchellh:</em> Docker launched today. It is incredible. They’re also working RIGHT NOW on a Vagrant provider. LXC is COMING!!
|
||||
</section>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<section class="contentblock twitterblock">
|
||||
<img src="https://si0.twimg.com/profile_images/1108290260/Adam_Jacob-114x150_original_bigger.jpg">
|
||||
<em>Adam Jacob@adamhjk:</em> Docker is clearly the right idea. @solomonstre absolutely killed it. Containerized app deployment is the future, I think.
|
||||
<em>Adam Jacob @adamhjk:</em> Docker is clearly the right idea. @solomonstre absolutely killed it. Containerized app deployment is the future, I think.
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,13 +214,13 @@
|
||||
<div class="span6">
|
||||
<section class="contentblock twitterblock">
|
||||
<img src="https://si0.twimg.com/profile_images/14872832/twitter_pic_bigger.jpg">
|
||||
<em>Matt Townsend@mtownsend:</em> I have a serious code crush on docker.io - it's Lego for PaaS. Motherfucking awesome Lego.
|
||||
<em>Matt Townsend @mtownsend:</em> I have a serious code crush on docker.io - it's Lego for PaaS. Motherfucking awesome Lego.
|
||||
</section>
|
||||
</div>
|
||||
<div class="span6">
|
||||
<section class="contentblock twitterblock">
|
||||
<img src="https://si0.twimg.com/profile_images/1312352395/rupert-259x300_bigger.jpg">
|
||||
<em>Rob Harrop@robertharrop:</em> Impressed by @getdocker - it's all kinds of magic. Serious rethink of AWS architecture happening @skillsmatter.
|
||||
<em>Rob Harrop @robertharrop:</em> Impressed by @getdocker - it's all kinds of magic. Serious rethink of AWS architecture happening @skillsmatter.
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +314,10 @@
|
||||
<footer id="footer" class="footer">
|
||||
<div class="row">
|
||||
<div class="span12">
|
||||
|
||||
<div class="tbox textright forceleftmargin social links pull-right">
|
||||
<a class="twitter" href="http://twitter.com/getdocker">Twitter</a>
|
||||
<a class="github" href="https://github.com/dotcloud/docker/">GitHub</a>
|
||||
</div>
|
||||
Docker is a project by <a href="http://www.dotcloud.com">dotCloud</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
)
|
||||
|
||||
func getKernelVersion() (*utils.KernelVersionInfo, error) {
|
||||
return nil, fmt.Errorf("Kernel version detection is not available on darwin")
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// FIXME: Move this to utils package
|
||||
func getKernelVersion() (*utils.KernelVersionInfo, error) {
|
||||
var (
|
||||
uts syscall.Utsname
|
||||
flavor string
|
||||
kernel, major, minor int
|
||||
err error
|
||||
)
|
||||
|
||||
if err := syscall.Uname(&uts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
release := make([]byte, len(uts.Release))
|
||||
|
||||
i := 0
|
||||
for _, c := range uts.Release {
|
||||
release[i] = byte(c)
|
||||
i++
|
||||
}
|
||||
|
||||
// Remove the \x00 from the release for Atoi to parse correctly
|
||||
release = release[:bytes.IndexByte(release, 0)]
|
||||
|
||||
tmp := strings.SplitN(string(release), "-", 2)
|
||||
tmp2 := strings.SplitN(tmp[0], ".", 3)
|
||||
|
||||
if len(tmp2) > 0 {
|
||||
kernel, err = strconv.Atoi(tmp2[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmp2) > 1 {
|
||||
major, err = strconv.Atoi(tmp2[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmp2) > 2 {
|
||||
minor, err = strconv.Atoi(tmp2[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmp) == 2 {
|
||||
flavor = tmp[1]
|
||||
} else {
|
||||
flavor = ""
|
||||
}
|
||||
|
||||
return &utils.KernelVersionInfo{
|
||||
Kernel: kernel,
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Flavor: flavor,
|
||||
}, nil
|
||||
}
|
||||
@@ -86,14 +86,23 @@ func (graph *Graph) Get(name string) (*Image, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if img.Id != id {
|
||||
return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.Id)
|
||||
if img.ID != id {
|
||||
return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.ID)
|
||||
}
|
||||
img.graph = graph
|
||||
if img.Size == 0 {
|
||||
root, err := img.root()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := StoreSize(img, root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
graph.lockSumMap.Lock()
|
||||
defer graph.lockSumMap.Unlock()
|
||||
if _, exists := graph.checksumLock[img.Id]; !exists {
|
||||
graph.checksumLock[img.Id] = &sync.Mutex{}
|
||||
if _, exists := graph.checksumLock[img.ID]; !exists {
|
||||
graph.checksumLock[img.ID] = &sync.Mutex{}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
@@ -101,7 +110,7 @@ func (graph *Graph) Get(name string) (*Image, error) {
|
||||
// Create creates a new image and registers it in the graph.
|
||||
func (graph *Graph) Create(layerData Archive, container *Container, comment, author string, config *Config) (*Image, error) {
|
||||
img := &Image{
|
||||
Id: GenerateId(),
|
||||
ID: GenerateID(),
|
||||
Comment: comment,
|
||||
Created: time.Now(),
|
||||
DockerVersion: VERSION,
|
||||
@@ -111,7 +120,7 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut
|
||||
}
|
||||
if container != nil {
|
||||
img.Parent = container.Image
|
||||
img.Container = container.Id
|
||||
img.Container = container.ID
|
||||
img.ContainerConfig = *container.Config
|
||||
}
|
||||
if err := graph.Register(layerData, layerData != nil, img); err != nil {
|
||||
@@ -124,12 +133,12 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut
|
||||
// Register imports a pre-existing image into the graph.
|
||||
// FIXME: pass img as first argument
|
||||
func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
|
||||
if err := ValidateId(img.Id); err != nil {
|
||||
if err := ValidateID(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
// (This is a convenience to save time. Race conditions are taken care of by os.Rename)
|
||||
if graph.Exists(img.Id) {
|
||||
return fmt.Errorf("Image %s already exists", img.Id)
|
||||
if graph.Exists(img.ID) {
|
||||
return fmt.Errorf("Image %s already exists", img.ID)
|
||||
}
|
||||
tmp, err := graph.Mktemp("")
|
||||
defer os.RemoveAll(tmp)
|
||||
@@ -140,12 +149,12 @@ func (graph *Graph) Register(layerData Archive, store bool, img *Image) error {
|
||||
return err
|
||||
}
|
||||
// Commit
|
||||
if err := os.Rename(tmp, graph.imageRoot(img.Id)); err != nil {
|
||||
if err := os.Rename(tmp, graph.imageRoot(img.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
img.graph = graph
|
||||
graph.idIndex.Add(img.Id)
|
||||
graph.checksumLock[img.Id] = &sync.Mutex{}
|
||||
graph.idIndex.Add(img.ID)
|
||||
graph.checksumLock[img.ID] = &sync.Mutex{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,14 +182,14 @@ func (graph *Graph) TempLayerArchive(id string, compression Compression, output
|
||||
// Mktemp creates a temporary sub-directory inside the graph's filesystem.
|
||||
func (graph *Graph) Mktemp(id string) (string, error) {
|
||||
if id == "" {
|
||||
id = GenerateId()
|
||||
id = GenerateID()
|
||||
}
|
||||
tmp, err := graph.tmp()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Couldn't create temp: %s", err)
|
||||
}
|
||||
if tmp.Exists(id) {
|
||||
return "", fmt.Errorf("Image %d already exists", id)
|
||||
return "", fmt.Errorf("Image %s already exists", id)
|
||||
}
|
||||
return tmp.imageRoot(id), nil
|
||||
}
|
||||
@@ -230,7 +239,7 @@ func (graph *Graph) Map() (map[string]*Image, error) {
|
||||
}
|
||||
images := make(map[string]*Image, len(all))
|
||||
for _, image := range all {
|
||||
images[image.Id] = image
|
||||
images[image.ID] = image
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
@@ -273,10 +282,10 @@ func (graph *Graph) ByParent() (map[string][]*Image, error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if children, exists := byParent[parent.Id]; exists {
|
||||
byParent[parent.Id] = []*Image{image}
|
||||
if children, exists := byParent[parent.ID]; exists {
|
||||
byParent[parent.ID] = []*Image{image}
|
||||
} else {
|
||||
byParent[parent.Id] = append(children, image)
|
||||
byParent[parent.ID] = append(children, image)
|
||||
}
|
||||
})
|
||||
return byParent, err
|
||||
@@ -293,8 +302,8 @@ func (graph *Graph) Heads() (map[string]*Image, error) {
|
||||
err = graph.WalkAll(func(image *Image) {
|
||||
// If it's not in the byParent lookup table, then
|
||||
// it's not a parent -> so it's a head!
|
||||
if _, exists := byParent[image.Id]; !exists {
|
||||
heads[image.Id] = image
|
||||
if _, exists := byParent[image.ID]; !exists {
|
||||
heads[image.ID] = image
|
||||
}
|
||||
})
|
||||
return heads, err
|
||||
@@ -317,11 +326,11 @@ func (graph *Graph) getStoredChecksums() (map[string]string, error) {
|
||||
}
|
||||
|
||||
func (graph *Graph) storeChecksums(checksums map[string]string) error {
|
||||
checksumJson, err := json.Marshal(checksums)
|
||||
checksumJSON, err := json.Marshal(checksums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(graph.Root, "checksums"), checksumJson, 0600); err != nil {
|
||||
if err := ioutil.WriteFile(path.Join(graph.Root, "checksums"), checksumJSON, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -34,14 +34,14 @@ func TestInterruptedRegister(t *testing.T) {
|
||||
defer os.RemoveAll(graph.Root)
|
||||
badArchive, w := io.Pipe() // Use a pipe reader as a fake archive which never yields data
|
||||
image := &Image{
|
||||
Id: GenerateId(),
|
||||
ID: GenerateID(),
|
||||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
go graph.Register(badArchive, false, image)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling)
|
||||
if _, err := graph.Get(image.Id); err == nil {
|
||||
if _, err := graph.Get(image.ID); err == nil {
|
||||
t.Fatal("Image should not exist after Register is interrupted")
|
||||
}
|
||||
// Registering the same image again should succeed if the first register was interrupted
|
||||
@@ -67,7 +67,7 @@ func TestGraphCreate(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateId(image.Id); err != nil {
|
||||
if err := ValidateID(image.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if image.Comment != "Testing" {
|
||||
@@ -91,7 +91,7 @@ func TestRegister(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
image := &Image{
|
||||
Id: GenerateId(),
|
||||
ID: GenerateID(),
|
||||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
@@ -104,11 +104,11 @@ func TestRegister(t *testing.T) {
|
||||
} else if l := len(images); l != 1 {
|
||||
t.Fatalf("Wrong number of images. Should be %d, not %d", 1, l)
|
||||
}
|
||||
if resultImg, err := graph.Get(image.Id); err != nil {
|
||||
if resultImg, err := graph.Get(image.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if resultImg.Id != image.Id {
|
||||
t.Fatalf("Wrong image ID. Should be '%s', not '%s'", image.Id, resultImg.Id)
|
||||
if resultImg.ID != image.ID {
|
||||
t.Fatalf("Wrong image ID. Should be '%s', not '%s'", image.ID, resultImg.ID)
|
||||
}
|
||||
if resultImg.Comment != image.Comment {
|
||||
t.Fatalf("Wrong image comment. Should be '%s', not '%s'", image.Comment, resultImg.Comment)
|
||||
@@ -156,7 +156,7 @@ func TestDeletePrefix(t *testing.T) {
|
||||
graph := tempGraph(t)
|
||||
defer os.RemoveAll(graph.Root)
|
||||
img := createTestImage(graph, t)
|
||||
if err := graph.Delete(utils.TruncateId(img.Id)); err != nil {
|
||||
if err := graph.Delete(utils.TruncateID(img.ID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 0)
|
||||
@@ -187,21 +187,29 @@ func TestDelete(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
if err := graph.Delete(img.Id); err != nil {
|
||||
if err := graph.Delete(img.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 0)
|
||||
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test 2 create (same name) / 1 delete
|
||||
img1, err := graph.Create(archive, nil, "Testing", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = graph.Create(archive, nil, "Testing", "", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 2)
|
||||
if err := graph.Delete(img1.Id); err != nil {
|
||||
if err := graph.Delete(img1.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
@@ -212,11 +220,15 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test delete twice (pull -> rm -> pull -> rm)
|
||||
if err := graph.Register(archive, false, img1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := graph.Delete(img1.Id); err != nil {
|
||||
if err := graph.Delete(img1.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Docker principles
|
||||
|
||||
In the design and development of Docker we try to follow these principles:
|
||||
|
||||
(Work in progress)
|
||||
|
||||
* Don't try to replace every tool. Instead, be an ingredient to improve them.
|
||||
* Less code is better.
|
||||
* Less components is better. Do you really need to add one more class?
|
||||
* 50 lines of straightforward, readable code is better than 10 lines of magic that nobody can understand.
|
||||
* Don't do later what you can do now. "//FIXME: refactor" is not acceptable in new code.
|
||||
* When hesitating between 2 options, choose the one that is easier to reverse.
|
||||
* No is temporary, Yes is forever. If you're not sure about a new feature, say no. You can change your mind later.
|
||||
* Containers must be portable to the greatest possible number of machines. Be suspicious of any change which makes machines less interchangeable.
|
||||
* The less moving parts in a container, the better.
|
||||
* Don't merge it unless you document it.
|
||||
* Don't document it unless you can keep it up-to-date.
|
||||
* Don't merge it unless you test it!
|
||||
* Everyone's problem is slightly different. Focus on the part that is the same for everyone, and solve that.
|
||||
@@ -3,8 +3,8 @@ This directory contains material helpful for hacking on docker.
|
||||
make hack
|
||||
=========
|
||||
|
||||
Set up an Ubuntu 13.04 virtual machine for developers including kernel 3.8
|
||||
and buildbot. The environment is setup in a way that can be used through
|
||||
Set up an Ubuntu 12.04 virtual machine for developers including kernel 3.8
|
||||
go1.1 and buildbot. The environment is setup in a way that can be used through
|
||||
the usual go workflow and/or the root Makefile. You can either edit on
|
||||
your host, or inside the VM (using make ssh-dev) and run and test docker
|
||||
inside the VM.
|
||||
@@ -22,6 +22,7 @@ developers are inconvenienced by the failure.
|
||||
|
||||
When running 'make hack' at the docker root directory, it spawns a virtual
|
||||
machine in the background running a buildbot instance and adds a git
|
||||
post-commit hook that automatically run docker tests for you.
|
||||
post-commit hook that automatically run docker tests for you each time you
|
||||
commit in your local docker repository.
|
||||
|
||||
You can check your buildbot instance at http://192.168.33.21:8010/waterfall
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
## A maintainer's guide to releasing Docker
|
||||
|
||||
So you're in charge of a docker release? Cool. Here's what to do.
|
||||
|
||||
If your experience deviates from this document, please document the changes to keep it
|
||||
up-to-date.
|
||||
|
||||
|
||||
### 1. Pull from master and create a release branch
|
||||
|
||||
```bash
|
||||
$ git checkout master
|
||||
$ git pull
|
||||
$ git checkout -b bump_$VERSION
|
||||
```
|
||||
|
||||
### 2. Update CHANGELOG.md
|
||||
|
||||
You can run this command for reference:
|
||||
|
||||
```bash
|
||||
LAST_VERSION=$(git tag | grep -E "v[0-9\.]+$" | sort -nr | head -n 1)
|
||||
git log $LAST_VERSION..HEAD
|
||||
```
|
||||
|
||||
Each change should be formatted as ```BULLET CATEGORY: DESCRIPTION```
|
||||
|
||||
* BULLET is either ```-```, ```+``` or ```*```, to indicate a bugfix,
|
||||
new feature or upgrade, respectively.
|
||||
|
||||
* CATEGORY should describe which part of the project is affected.
|
||||
Valid categories are:
|
||||
* Runtime
|
||||
* Remote API
|
||||
* Builder
|
||||
* Documentation
|
||||
* Hack
|
||||
|
||||
* DESCRIPTION: a concise description of the change that is relevant to the end-user,
|
||||
using the present tense.
|
||||
Changes should be described in terms of how they affect the user, for example "new feature
|
||||
X which allows Y", "fixed bug which caused X", "increased performance of Y".
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
```
|
||||
+ Builder: 'docker build -t FOO' applies the tag FOO to the newly built container.
|
||||
* Runtime: improve detection of kernel version
|
||||
- Remote API: fix a bug in the optional unix socket transport
|
||||
```
|
||||
|
||||
### 3. Change VERSION in commands.go
|
||||
|
||||
### 4. Run all tests
|
||||
|
||||
### 5. Commit and create a pull request
|
||||
|
||||
```bash
|
||||
$ git add commands.go CHANGELOG.md
|
||||
$ git commit -m "Bump version to $VERSION"
|
||||
$ git push origin bump_$VERSION
|
||||
```
|
||||
|
||||
### 6. Get 2 other maintainers to validate the pull request
|
||||
|
||||
### 7. Merge the pull request and apply tags
|
||||
|
||||
```bash
|
||||
$ git checkout master
|
||||
$ git merge bump_$VERSION
|
||||
$ git tag -a v$VERSION # Don't forget the v!
|
||||
$ git tag -f -a latest
|
||||
$ git push
|
||||
$ git push --tags
|
||||
```
|
||||
|
||||
### 8. Publish binaries
|
||||
|
||||
To run this you will need access to the release credentials.
|
||||
Get them from [the infrastructure maintainers](https://github.com/dotcloud/docker/blob/master/hack/infrastructure/MAINTAINERS).
|
||||
|
||||
```bash
|
||||
$ RELEASE_IMAGE=image_provided_by_infrastructure_maintainers
|
||||
$ BUILD=$(docker run -d -e RELEASE_PPA=0 $RELEASE_IMAGE)
|
||||
```
|
||||
|
||||
This will do 2 things:
|
||||
|
||||
* It will build and upload the binaries on http://get.docker.io
|
||||
* It will *test* the release on our Ubuntu PPA (a PPA is a community repository for ubuntu packages)
|
||||
|
||||
Wait for the build to complete.
|
||||
|
||||
```bash
|
||||
$ docker wait $BUILD # This should print 0. If it doesn't, your build failed.
|
||||
```
|
||||
|
||||
Check that the output looks OK. Here's an example of a correct output:
|
||||
|
||||
```bash
|
||||
$ docker logs 2>&1 b4e7c8299d73 | grep -e 'Public URL' -e 'Successfully uploaded'
|
||||
Public URL of the object is: http://get.docker.io.s3.amazonaws.com/builds/Linux/x86_64/docker-v0.4.7.tgz
|
||||
Public URL of the object is: http://get.docker.io.s3.amazonaws.com/builds/Linux/x86_64/docker-latest.tgz
|
||||
Successfully uploaded packages.
|
||||
```
|
||||
|
||||
If you don't see 3 lines similar to this, something might be wrong. Check the full logs and try again.
|
||||
|
||||
|
||||
### 9. Publish Ubuntu packages
|
||||
|
||||
If everything went well in the previous step, you can finalize the release by submitting the Ubuntu packages.
|
||||
|
||||
```bash
|
||||
$ RELEASE_IMAGE=image_provided_by_infrastructure_maintainers
|
||||
$ docker run -e RELEASE_PPA=1 $RELEASE_IMAGE
|
||||
```
|
||||
|
||||
If that goes well, congratulations! You're done.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Docker: what's next?
|
||||
|
||||
This document is a high-level overview of where we want to take Docker next.
|
||||
It is a curated selection of planned improvements which are either important, difficult, or both.
|
||||
|
||||
For a more complete view of planned and requested improvements, see [the Github issues](https://github.com/dotcloud/docker/issues).
|
||||
|
||||
Tu suggest changes to the roadmap, including additions, please write the change as if it were already in effect, and make a pull request.
|
||||
|
||||
Broader kernel support
|
||||
----------------------
|
||||
|
||||
Our goal is to make Docker run everywhere, but currently Docker requires [Linux version 3.8 or higher with lxc and aufs support](http://docs.docker.io/en/latest/installation/kernel.html). If you're deploying new machines for the purpose of running Docker, this is a fairly easy requirement to meet.
|
||||
However, if you're adding Docker to an existing deployment, you may not have the flexibility to update and patch the kernel.
|
||||
|
||||
Expanding Docker's kernel support is a priority. This includes running on older kernel versions,
|
||||
but also on kernels with no AUFS support, or with incomplete lxc capabilities.
|
||||
|
||||
|
||||
Cross-architecture support
|
||||
--------------------------
|
||||
|
||||
Our goal is to make Docker run everywhere. However currently Docker only runs on x86_64 systems.
|
||||
We plan on expanding architecture support, so that Docker containers can be created and used on more architectures.
|
||||
|
||||
|
||||
Even more integrations
|
||||
----------------------
|
||||
|
||||
We want Docker to be the secret ingredient that makes your existing tools more awesome.
|
||||
Thanks to this philosophy, Docker has already been integrated with
|
||||
[Puppet](http://forge.puppetlabs.com/garethr/docker), [Chef](http://www.opscode.com/chef),
|
||||
[Openstack Nova](https://github.com/dotcloud/openstack-docker), [Jenkins](https://github.com/georgebashi/jenkins-docker-plugin),
|
||||
[DotCloud sandbox](http://github.com/dotcloud/sandbox), [Pallet](https://github.com/pallet/pallet-docker),
|
||||
[Strider CI](http://blog.frozenridge.co/next-generation-continuous-integration-deployment-with-dotclouds-docker-and-strider/)
|
||||
and even [Heroku buildpacks](https://github.com/progrium/buildstep).
|
||||
|
||||
Expect Docker to integrate with even more of your favorite tools going forward, including:
|
||||
|
||||
* Alternative storage backends such as ZFS, LVM or [BTRFS](github.com/dotcloud/docker/issues/443)
|
||||
* Alternative containerization backends such as [OpenVZ](http://openvz.org), Solaris Zones, BSD Jails and even plain Chroot.
|
||||
* Process managers like [Supervisord](http://supervisord.org/), [Runit](http://smarden.org/runit/), [Gaffer](https://gaffer.readthedocs.org/en/latest/#gaffer) and [Systemd](http://www.freedesktop.org/wiki/Software/systemd/)
|
||||
* Build and integration tools like Make, Maven, Scons, Jenkins, Buildbot and Cruise Control.
|
||||
* Configuration management tools like [Puppet](http://puppetlabs.com), [Chef](http://www.opscode.com/chef/) and [Salt](http://saltstack.org)
|
||||
* Personal development environments like [Vagrant](http://vagrantup.com), [Boxen](http://boxen.github.com/), [Koding](http://koding.com) and [Cloud9](http://c9.io).
|
||||
* Orchestration tools like [Zookeeper](http://zookeeper.apache.org/), [Mesos](http://incubator.apache.org/mesos/) and [Galaxy](https://github.com/ning/galaxy)
|
||||
* Infrastructure deployment tools like [Openstack](http://openstack.org), [Apache Cloudstack](http://apache.cloudstack.org), [Ganeti](https://code.google.com/p/ganeti/)
|
||||
|
||||
|
||||
Plugin API
|
||||
----------
|
||||
|
||||
We want Docker to run everywhere, and to integrate with every devops tool.
|
||||
Those are ambitious goals, and the only way to reach them is with the Docker community.
|
||||
For the community to participate fully, we need an API which allows Docker to be deeply and easily customized.
|
||||
|
||||
We are working on a plugin API which will make Docker very, very customization-friendly.
|
||||
We believe it will facilitate the integrations listed above - and many more we didn't even think about.
|
||||
|
||||
Let us know if you want to start playing with the API before it's generally available.
|
||||
|
||||
|
||||
Externally mounted volumes
|
||||
--------------------------
|
||||
|
||||
In 0.3 we [introduced data volumes](https://github.com/dotcloud/docker/wiki/Docker-0.3.0-release-note%2C-May-6-2013#data-volumes),
|
||||
a great mechanism for manipulating persistent data such as database files, log files, etc.
|
||||
|
||||
Data volumes can be shared between containers, a powerful capability [which allows many advanced use cases](http://docs.docker.io/en/latest/examples/couchdb_data_volumes.html). In the future it will also be possible to share volumes between a container and the underlying host. This will make certain scenarios much easier, such as using a high-performance storage backend for your production database,
|
||||
making live development changes available to a container, etc.
|
||||
|
||||
|
||||
Better documentation
|
||||
--------------------
|
||||
|
||||
We believe that great documentation is worth 10 features. We are often told that "Docker's documentation is great for a 2-month old project".
|
||||
Our goal is to make it great, period.
|
||||
|
||||
If you have feedback on how to improve our documentation, please get in touch by replying to this email,
|
||||
or by [filing an issue](https://github.com/dotcloud/docker/issues). We always appreciate it!
|
||||
|
||||
|
||||
Production-ready
|
||||
----------------
|
||||
|
||||
Docker is still alpha software, and not suited for production.
|
||||
We are working hard to get there, and we are confident that it will be possible within a few months.
|
||||
|
||||
|
||||
Advanced port redirections
|
||||
--------------------------
|
||||
|
||||
Docker currently supports 2 flavors of port redirection: STATIC->STATIC (eg. "redirect public port 80 to private port 80")
|
||||
and RANDOM->STATIC (eg. "redirect any public port to private port 80").
|
||||
|
||||
With these 2 flavors, docker can support the majority of backend programs out there. But some applications have more exotic
|
||||
requirements, generally to implement custom clustering techniques. These applications include Hadoop, MongoDB, Riak, RabbitMQ,
|
||||
Disco, and all programs relying on Erlang's OTP.
|
||||
|
||||
To support these applications, Docker needs to support more advanced redirection flavors, including:
|
||||
|
||||
* RANDOM->RANDOM
|
||||
* STATIC1->STATIC2
|
||||
|
||||
These flavors should be implemented without breaking existing semantics, if at all possible.
|
||||
@@ -2,7 +2,7 @@
|
||||
# vi: set ft=ruby :
|
||||
|
||||
BOX_NAME = "ubuntu-dev"
|
||||
BOX_URI = "http://cloud-images.ubuntu.com/raring/current/raring-server-cloudimg-vagrant-amd64-disk1.box"
|
||||
BOX_URI = "http://files.vagrantup.com/precise64.box"
|
||||
VM_IP = "192.168.33.21"
|
||||
USER = "vagrant"
|
||||
GOPATH = "/data/docker"
|
||||
@@ -21,14 +21,15 @@ Vagrant::Config.run do |config|
|
||||
# Touch for makefile
|
||||
pkg_cmd = "touch #{DOCKER_PATH}; "
|
||||
# Install docker dependencies
|
||||
pkg_cmd << "export DEBIAN_FRONTEND=noninteractive; apt-get -qq update; " \
|
||||
"apt-get install -q -y lxc bsdtar git golang make linux-image-extra-3.8.0-19-generic; " \
|
||||
pkg_cmd << "apt-get update -qq; apt-get install -y python-software-properties; " \
|
||||
"add-apt-repository -y ppa:dotcloud/docker-golang/ubuntu; apt-get update -qq; " \
|
||||
"apt-get install -y linux-image-generic-lts-raring lxc git aufs-tools golang-stable make; " \
|
||||
"chown -R #{USER}.#{USER} #{GOPATH}; " \
|
||||
"install -m 0664 #{CFG_PATH}/bash_profile /home/#{USER}/.bash_profile"
|
||||
config.vm.provision :shell, :inline => pkg_cmd
|
||||
# Deploy buildbot CI
|
||||
pkg_cmd = "apt-get install -q -y python-dev python-pip supervisor; " \
|
||||
"pip install -r #{CFG_PATH}/requirements.txt; " \
|
||||
"pip install -q -r #{CFG_PATH}/requirements.txt; " \
|
||||
"chown #{USER}.#{USER} /data; cd /data; " \
|
||||
"#{CFG_PATH}/setup.sh #{USER} #{GOPATH} #{DOCKER_PATH} #{CFG_PATH} #{BUILDBOT_PATH}"
|
||||
config.vm.provision :shell, :inline => pkg_cmd
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# This will build a container capable of producing an official binary build of docker and
|
||||
# uploading it to S3
|
||||
# DESCRIPTION Build a container capable of producing official binary and
|
||||
# PPA packages and uploading them to S3 and Launchpad
|
||||
# VERSION 1.2
|
||||
# DOCKER_VERSION 0.4
|
||||
# AUTHOR Solomon Hykes <solomon@dotcloud.com>
|
||||
# Daniel Mizyrycki <daniel@dotcloud.net>
|
||||
# BUILD_CMD docker build -t dockerbuilder .
|
||||
# RUN_CMD docker run -e AWS_ID="$AWS_ID" -e AWS_KEY="$AWS_KEY" -e GPG_KEY="$GPG_KEY" dockerbuilder
|
||||
#
|
||||
#
|
||||
from ubuntu:12.04
|
||||
maintainer Solomon Hykes <solomon@dotcloud.com>
|
||||
# Workaround the upstart issue
|
||||
@@ -8,24 +16,18 @@ run ln -s /bin/true /sbin/initctl
|
||||
# Enable universe and gophers PPA
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q python-software-properties
|
||||
run add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe"
|
||||
run add-apt-repository -y ppa:gophers/go/ubuntu
|
||||
run add-apt-repository -y ppa:dotcloud/docker-golang/ubuntu
|
||||
run apt-get update
|
||||
# Packages required to checkout, build and upload docker
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd curl
|
||||
run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.1.linux-amd64.tar.gz
|
||||
run tar -C /usr/local -xzf /go.tar.gz
|
||||
run echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc
|
||||
run echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q build-essential
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q git build-essential
|
||||
# Packages required to build an ubuntu package
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang-stable
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q debhelper
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q autotools-dev
|
||||
run apt-get install -y -q devscripts
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang-stable debhelper autotools-dev devscripts
|
||||
# Copy dockerbuilder files into the container
|
||||
add . /src
|
||||
run cp /src/dockerbuilder /usr/local/bin/ && chmod +x /usr/local/bin/dockerbuilder
|
||||
run cp /src/s3cfg /.s3cfg
|
||||
cmd ["dockerbuilder"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Daniel Mizyrycki <daniel@dotcloud.com>
|
||||
@@ -13,12 +13,10 @@ fi
|
||||
|
||||
export REVISION=$1
|
||||
|
||||
if [ -z "$AWS_ID" ]; then
|
||||
echo "Warning: environment variable AWS_ID is not set. Won't upload to S3."
|
||||
fi
|
||||
|
||||
if [ -z "$AWS_KEY" ]; then
|
||||
echo "Warning: environment variable AWS_KEY is not set. Won't upload to S3."
|
||||
if [ -z "$AWS_ID" -o -z "$AWS_KEY" ]; then
|
||||
echo "Warning: either AWS_ID or AWS_KEY environment variable not set. Won't upload to S3."
|
||||
else
|
||||
/bin/echo -e "[default]\naccess_key = $AWS_ID\nsecret_key = $AWS_KEY\n" > /.s3cfg
|
||||
fi
|
||||
|
||||
if [ -z "$GPG_KEY" ]; then
|
||||
@@ -35,6 +33,9 @@ else
|
||||
make release RELEASE_VERSION=$REVISION
|
||||
fi
|
||||
|
||||
# Remove credentials from container
|
||||
rm -f /.s3cfg
|
||||
|
||||
if [ -z "$NO_UBUNTU" ]; then
|
||||
export PATH=`echo $PATH | sed 's#/usr/local/go/bin:##g'`
|
||||
(cd packaging/ubuntu && make ubuntu)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[default]
|
||||
access_key = $AWS_ID
|
||||
secret_key = $AWS_KEY
|
||||
@@ -13,12 +13,13 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Id string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
@@ -29,6 +30,7 @@ type Image struct {
|
||||
Config *Config `json:"config,omitempty"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
graph *Graph
|
||||
Size int64
|
||||
}
|
||||
|
||||
func LoadImage(root string) (*Image, error) {
|
||||
@@ -42,18 +44,17 @@ func LoadImage(root string) (*Image, error) {
|
||||
if err := json.Unmarshal(jsonData, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateId(img.Id); err != nil {
|
||||
if err := ValidateID(img.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check that the filesystem layer exists
|
||||
if stat, err := os.Stat(layerPath(root)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("Couldn't load image %s: no filesystem layer", img.Id)
|
||||
} else {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Couldn't load image %s: no filesystem layer", img.ID)
|
||||
}
|
||||
return nil, err
|
||||
} else if !stat.IsDir() {
|
||||
return nil, fmt.Errorf("Couldn't load image %s: %s is not a directory", img.Id, layerPath(root))
|
||||
return nil, fmt.Errorf("Couldn't load image %s: %s is not a directory", img.ID, layerPath(root))
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
@@ -61,7 +62,7 @@ func LoadImage(root string) (*Image, error) {
|
||||
func StoreImage(img *Image, layerData Archive, root string, store bool) error {
|
||||
// Check that root doesn't already exist
|
||||
if _, err := os.Stat(root); err == nil {
|
||||
return fmt.Errorf("Image %s already exists", img.Id)
|
||||
return fmt.Errorf("Image %s already exists", img.ID)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@@ -91,10 +92,24 @@ func StoreImage(img *Image, layerData Archive, root string, store bool) error {
|
||||
defer file.Close()
|
||||
layerData = file
|
||||
}
|
||||
|
||||
if err := Untar(layerData, layer); err != nil {
|
||||
return err
|
||||
// If layerData is not nil, unpack it into the new layer
|
||||
if layerData != nil {
|
||||
if err := Untar(layerData, layer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return StoreSize(img, root)
|
||||
}
|
||||
|
||||
func StoreSize(img *Image, root string) error {
|
||||
layer := layerPath(root)
|
||||
|
||||
filepath.Walk(layer, func(path string, fileInfo os.FileInfo, err error) error {
|
||||
img.Size += fileInfo.Size()
|
||||
return nil
|
||||
})
|
||||
|
||||
// Store the json ball
|
||||
jsonData, err := json.Marshal(img)
|
||||
if err != nil {
|
||||
@@ -127,6 +142,8 @@ func MountAUFS(ro []string, rw string, target string) error {
|
||||
}
|
||||
branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches)
|
||||
|
||||
branches += ",xino=/dev/shm/aufs.xino"
|
||||
|
||||
//if error, try to load aufs kernel module
|
||||
if err := mount("none", target, "aufs", 0, branches); err != nil {
|
||||
log.Printf("Kernel does not support AUFS, trying to load the AUFS module with modprobe...")
|
||||
@@ -181,11 +198,11 @@ func (image *Image) Changes(rw string) ([]Change, error) {
|
||||
return Changes(layers, rw)
|
||||
}
|
||||
|
||||
func (image *Image) ShortId() string {
|
||||
return utils.TruncateId(image.Id)
|
||||
func (image *Image) ShortID() string {
|
||||
return utils.TruncateID(image.ID)
|
||||
}
|
||||
|
||||
func ValidateId(id string) error {
|
||||
func ValidateID(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("Image id can't be empty")
|
||||
}
|
||||
@@ -195,7 +212,7 @@ func ValidateId(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateId() string {
|
||||
func GenerateID() string {
|
||||
id := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, id)
|
||||
if err != nil {
|
||||
@@ -241,7 +258,7 @@ func (img *Image) layers() ([]string, error) {
|
||||
return nil, e
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return nil, fmt.Errorf("No layer found for image %s\n", img.Id)
|
||||
return nil, fmt.Errorf("No layer found for image %s\n", img.ID)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
@@ -276,7 +293,7 @@ func (img *Image) root() (string, error) {
|
||||
if img.graph == nil {
|
||||
return "", fmt.Errorf("Can't lookup root of unregistered image")
|
||||
}
|
||||
return img.graph.imageRoot(img.Id), nil
|
||||
return img.graph.imageRoot(img.ID), nil
|
||||
}
|
||||
|
||||
// Return the path of an image's layer
|
||||
@@ -289,8 +306,8 @@ func (img *Image) layer() (string, error) {
|
||||
}
|
||||
|
||||
func (img *Image) Checksum() (string, error) {
|
||||
img.graph.checksumLock[img.Id].Lock()
|
||||
defer img.graph.checksumLock[img.Id].Unlock()
|
||||
img.graph.checksumLock[img.ID].Lock()
|
||||
defer img.graph.checksumLock[img.ID].Unlock()
|
||||
|
||||
root, err := img.root()
|
||||
if err != nil {
|
||||
@@ -301,7 +318,7 @@ func (img *Image) Checksum() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if checksum, ok := checksums[img.Id]; ok {
|
||||
if checksum, ok := checksums[img.ID]; ok {
|
||||
return checksum, nil
|
||||
}
|
||||
|
||||
@@ -352,7 +369,7 @@ func (img *Image) Checksum() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
checksums[img.Id] = hash
|
||||
checksums[img.ID] = hash
|
||||
|
||||
// Dump the checksums to disc
|
||||
if err := img.graph.storeChecksums(checksums); err != nil {
|
||||
@@ -362,8 +379,17 @@ func (img *Image) Checksum() (string, error) {
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func (img *Image) getParentsSize(size int64) int64 {
|
||||
parentImage, err := img.GetParent()
|
||||
if err != nil || parentImage == nil {
|
||||
return size
|
||||
}
|
||||
size += parentImage.Size
|
||||
return parentImage.getParentsSize(size)
|
||||
}
|
||||
|
||||
// Build an Image object from raw json data
|
||||
func NewImgJson(src []byte) (*Image, error) {
|
||||
func NewImgJSON(src []byte) (*Image, error) {
|
||||
ret := &Image{}
|
||||
|
||||
utils.Debugf("Json string: {%s}\n", src)
|
||||
|
||||
@@ -19,7 +19,7 @@ lxc.network.flags = up
|
||||
lxc.network.link = {{.NetworkSettings.Bridge}}
|
||||
lxc.network.name = eth0
|
||||
lxc.network.mtu = 1500
|
||||
lxc.network.ipv4 = {{.NetworkSettings.IpAddress}}/{{.NetworkSettings.IpPrefixLen}}
|
||||
lxc.network.ipv4 = {{.NetworkSettings.IPAddress}}/{{.NetworkSettings.IPPrefixLen}}
|
||||
|
||||
# root filesystem
|
||||
{{$ROOTFS := .RootfsPath}}
|
||||
@@ -67,7 +67,11 @@ lxc.cgroup.devices.allow = c 10:200 rwm
|
||||
|
||||
|
||||
# standard mount point
|
||||
# WARNING: procfs is a known attack vector and should probably be disabled
|
||||
# if your userspace allows it. eg. see http://blog.zx2c4.com/749
|
||||
lxc.mount.entry = proc {{$ROOTFS}}/proc proc nosuid,nodev,noexec 0 0
|
||||
# WARNING: sysfs is a known attack vector and should probably be disabled
|
||||
# if your userspace allows it. eg. see http://bit.ly/T9CkqJ
|
||||
lxc.mount.entry = sysfs {{$ROOTFS}}/sys sysfs nosuid,nodev,noexec 0 0
|
||||
lxc.mount.entry = devpts {{$ROOTFS}}/dev/pts devpts newinstance,ptmxmode=0666,nosuid,noexec 0 0
|
||||
#lxc.mount.entry = varrun {{$ROOTFS}}/var/run tmpfs mode=755,size=4096k,nosuid,nodev,noexec 0 0
|
||||
@@ -80,12 +84,16 @@ lxc.mount.entry = {{.SysInitPath}} {{$ROOTFS}}/sbin/init none bind,ro 0 0
|
||||
# In order to get a working DNS environment, mount bind (ro) the host's /etc/resolv.conf into the container
|
||||
lxc.mount.entry = {{.ResolvConfPath}} {{$ROOTFS}}/etc/resolv.conf none bind,ro 0 0
|
||||
{{if .Volumes}}
|
||||
{{range $virtualPath, $realPath := .GetVolumes}}
|
||||
lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,rw 0 0
|
||||
{{ $rw := .VolumesRW }}
|
||||
{{range $virtualPath, $realPath := .Volumes}}
|
||||
lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,{{ if index $rw $virtualPath }}rw{{else}}ro{{end}} 0 0
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
# drop linux capabilities (apply mainly to the user root in the container)
|
||||
# (Note: 'lxc.cap.keep' is coming soon and should replace this under the
|
||||
# security principle 'deny all unless explicitly permitted', see
|
||||
# http://sourceforge.net/mailarchive/message.php?msg_id=31054627 )
|
||||
lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod setfcap setpcap sys_admin sys_boot sys_module sys_nice sys_pacct sys_rawio sys_resource sys_time sys_tty_config
|
||||
|
||||
# limits
|
||||
|
||||