Compare commits

...

210 Commits
0.1.2 ... 0.4.0

Author SHA1 Message Date
Aanand Prasad
6dc1a404d5 Merge pull request #202 from orchardup/ship-0.4.0
Ship 0.4.0
2014-04-30 14:04:56 +01:00
Aanand Prasad
d1d4f47764 Ship 0.4.0 2014-04-30 11:53:39 +01:00
Aanand Prasad
3abce4259f Fix regression in handling of build errors 2014-04-30 11:53:23 +01:00
Aanand Prasad
c78b952c3d Merge pull request #201 from orchardup/better-log-printing
Better log printing
2014-04-29 10:57:56 +01:00
Ben Firshman
fff5e51426 Make log messages line up with each other 2014-04-29 09:31:57 +01:00
Ben Firshman
a724aa5717 Use name without project for log printing 2014-04-29 09:22:20 +01:00
Ben Firshman
10725136d8 Add tests for names on containers 2014-04-29 09:20:29 +01:00
Aanand Prasad
8f1793dd06 Merge pull request #186 from orchardup/update-docker-py-7f55a101f813f3e96413d1b577e98d9467b0bffc
WIP: Docker >=0.9 support, docker-py 0.3.1
2014-04-28 18:51:38 +01:00
Aanand Prasad
fd85be2c9e Update docker[-osx] version in install docs 2014-04-28 18:22:11 +01:00
Ben Firshman
24959209cc Update build status badge 2014-04-25 23:33:25 +01:00
Ben Firshman
29c9763feb Use Orchard to run integration tests 2014-04-25 23:24:05 +01:00
Ben Firshman
ca7151aeb1 Split tests into unit and integration 2014-04-25 22:58:21 +01:00
Aanand Prasad
6e932794f7 Fix regression when mounting volumes
Caused by
77fec67c60
2014-04-25 12:28:00 +01:00
Aanand Prasad
9e1dfcfb37 Update docker-py APIError imports 2014-04-23 18:20:33 +01:00
Aanand Prasad
5166b2c1a8 Update docker-py
Using commit:
b31bb4d879
2014-04-23 18:16:35 +01:00
Aanand Prasad
80991f1521 Set "VolumesFrom" when starting containers
This is necessary when working with Docker 0.10.0 and up. Fortunately,
we can set it both when creating and starting, and retain compatibility
with 0.8.x and 0.9.x.

recreate_containers() is now responsible for starting containers, as
well as creating them. This greatly simplifies usage of the Service
class.
2014-04-23 15:46:26 +01:00
Ben Firshman
b752a86589 Merge pull request #193 from damm/fix_scripts_build_linux
Ensure that `pwd`/dist exists and is writeable
2014-04-16 08:07:32 +01:00
Scott M. Likens
c2acceba4e Ensure pwd/dist exists always and is 777.
Fixes #192

Signed-off-by: Scott M. Likens <scott@likens.us>
2014-04-16 00:15:25 +00:00
Aanand Prasad
f8ee52ca2a Fix build output
docker-py now streams us the raw JSON events, so we have to replicate
the Docker client's progress logic.

On the bright side, we now have well-behaved progress bars when pulling
an image during `fig build` (no more ski slopes) and `fig up` (no more
silence).
2014-04-15 10:41:06 +01:00
Ben Firshman
2b245bdf9e Update to docker-py 0.3.1
From 7f55a101f8

This now requires Docker 0.9 or greater.
2014-04-15 10:41:06 +01:00
Ben Firshman
d7e01a23f8 Merge pull request #191 from orchardup/fix-one-off-containers-not-linking-to-service
Fix one-off containers not linking to service
2014-04-15 10:40:37 +01:00
Ben Firshman
4e20be9c66 Remove unused imports 2014-04-14 22:39:49 +01:00
Ben Firshman
94e15a9985 Fix one-off containers not linking to service
Closes #185.

Need to test this more thoroughly. We need a docker-py mock.
2014-04-14 22:29:03 +01:00
Ben Firshman
5061875fa9 Merge pull request #188 from shanejonas/fix/utf8-encoding
fix issue with utf8 encoding in logger stdout
2014-04-14 22:17:14 +01:00
Shane Jonas
d9782b2dd1 fix issue with utf8 encoding in logger stdout 2014-04-11 11:21:20 -07:00
Ben Firshman
530afba5cb Merge pull request #150 from muff1nman/utf8
Jekyll errors out with utf8 input
2014-04-06 16:01:32 +01:00
Aanand Prasad
4a90a7691b Merge pull request #180 from orchardup/better-error-message-for-broken-links
Improve error message when link does not exist
2014-04-05 17:29:08 +01:00
Ben Firshman
050f81e37c Improve error message when link does not exist 2014-04-04 13:06:52 +01:00
Ben Firshman
aecaf665f1 Merge pull request #164 from orchardup/friendlier-build-error
Friendlier build error
2014-03-28 23:05:46 +00:00
Ben Firshman
23a8938809 Merge pull request #165 from orchardup/number-one
Stop 'fig up' when a container exits
2014-03-28 23:03:00 +00:00
Ben Firshman
641c773476 Merge pull request #173 from colinmccune/master
Fixes the mac build script so it wont fail when the venv folder doesn't exist
2014-03-28 22:56:17 +00:00
Colin McCune
97be5b4cfb Updates the mac build script so it wont fail when the venv folder does not exist. 2014-03-27 20:22:05 -04:00
Aanand Prasad
76b7d0095d Merge pull request #171 from orchardup/scaling-down-removes-containers
Scaling down removes containers
2014-03-27 12:23:04 +00:00
sebastianneubauer
352ad7a38c Scaling down removes containers
Squashed version of #162.
Closes #121.
2014-03-26 18:28:10 +00:00
Aanand Prasad
401ea4e7a8 Merge pull request #166 from orchardup/utf8-fixes
Fix UnicodeEncodeErrors in output of 'build', 'run' and 'up'
2014-03-25 13:43:50 +00:00
Maurits van Mastrigt
710cd38591 Fix UnicodeEncodeErrors in output of 'build', 'run' and 'up'
Squashed version of #125.
Closes #112.
2014-03-25 13:32:03 +00:00
Aanand Prasad
859d4bb98b Stop 'fig up' when a container exits
Closes #1 ヽ(*・ω・)ノ
2014-03-25 13:19:32 +00:00
Aanand Prasad
a3374ac51d Update install instructions on homepage
Closes #161
2014-03-25 12:26:33 +00:00
Aanand Prasad
168b1909ae Friendlier build error
Hide the backtrace and show a comprehensible message.

Closes #160.
2014-03-25 12:20:05 +00:00
Ben Firshman
a96ab41739 Merge pull request #132 from kvz/privileged
Add support for privileged containers #123
2014-03-13 13:42:52 +00:00
Kevin van Zonneveld
0f5a56b3c2 Add support for privileged containers #123
This is required for mounting external volumes and
addresses errors such as `mount.nfs: Operation not permitted`

Be gentle, I don't normally use Python :)
2014-03-13 14:31:05 +01:00
Ben Firshman
c1d9e5156f Merge pull request #154 from talwai/patch-1
Update django.md: Fix link to fig.yml reference
2014-03-13 12:14:27 +00:00
Aaditya Talwai
b0ec54b6f7 Update django.md: Fix link to fig.yml reference 2014-03-12 15:45:32 -07:00
Andrew DeMaria
2360bf0eb9 Use utf8 when building docs 2014-03-08 18:28:41 -07:00
Ben Firshman
1a1a61a672 Merge pull request #148 from orchardup/only-self-link-in-one-off-containers
Only `fig run` containers link back to the service they're part of
2014-03-06 19:10:09 +00:00
Aanand Prasad
13c7380113 Only fig run containers link back to the service they're part of
Fixes #107.
2014-03-06 18:59:24 +00:00
Ben Firshman
3a48172332 Create CONTRIBUTING.md 2014-03-06 10:57:24 +00:00
Ben Firshman
14e778b3de Update version in install docs 2014-03-05 14:57:24 +00:00
Aanand Prasad
9164125177 Merge pull request #146 from orchardup/ship-0.3.2
Ship 0.3.2
2014-03-05 14:49:26 +00:00
Ben Firshman
2595f89519 Ship 0.3.2 2014-03-05 14:33:32 +00:00
Ben Firshman
a058c40dfb Merge pull request #143 from orchardup/expose-option
Support 'expose' config option, like docker's --expose
2014-03-05 14:30:17 +00:00
Ben Firshman
fc4b3d2771 Merge pull request #145 from marksteve/run-rm
Add option to remove container in `docker run` (Closes #137)
2014-03-05 11:58:44 +00:00
Mark Steve Samson
59cc9c9b68 Add option to remove container in docker run (Closes #137) 2014-03-05 09:03:06 +08:00
Aanand Prasad
d1a52d2b1a Remove unneeded 'ports' entries from WP and Django fig.ymls in docs 2014-03-04 21:54:10 +00:00
Aanand Prasad
5f5fbb3ea4 Display unpublished ports in 'fig ps' 2014-03-04 18:07:06 +00:00
Aanand Prasad
2d98071e55 Support 'expose' config option, like docker's --expose
Exposes ports to linked services without publishing them to the world.
2014-03-04 18:06:52 +00:00
Aanand Prasad
6a3d1d06b5 Check NetworkSettings.Ports instead of HostConfig.PortBindings in tests
The latter appears to be unreliable.
2014-03-04 17:58:42 +00:00
Ben Firshman
6813cb86a2 Update version in installation docs 2014-03-04 11:53:16 +00:00
Ben Firshman
f5cf9b60b2 Remove venv before building on OS X 2014-03-04 11:51:12 +00:00
Aanand Prasad
7cec029f03 Merge pull request #141 from orchardup/ship-0.3.1
Ship 0.3.1
2014-03-04 11:47:36 +00:00
Ben Firshman
4fbad941cb Ship 0.3.1 2014-03-04 11:40:39 +00:00
Aanand Prasad
d4bff56a61 Merge pull request #140 from orchardup/fix-ps-on-docker-0.8.1-with-no-command
Fix ps on Docker 0.8.1 when there is no command
2014-03-04 11:37:15 +00:00
Ben Firshman
f430b82b43 Fix ps on Docker 0.8.1 when there is no command
Fixes #138
2014-03-04 11:27:55 +00:00
Ben Firshman
71533791dd Merge pull request #139 from orchardup/fix-delete-volume
Fix delete volume
2014-03-04 11:19:35 +00:00
Ben Firshman
65f3411320 Merge pull request #133 from kvz/contributingdocs
Add a contributing page. Refs #123
2014-03-04 11:07:52 +00:00
Kevin van Zonneveld
b92651070f Add steps for contributing to README 2014-03-04 12:05:38 +01:00
Ben Firshman
5be8a37b7e Pass through standard remove_container options 2014-03-04 11:00:09 +00:00
Ben Firshman
044c348faf Add test for fig rm 2014-03-04 11:00:09 +00:00
Ben Firshman
465c7d569c Improve CLI test names 2014-03-04 11:00:09 +00:00
Ben Firshman
2ca0e7954a Add --force option to fig rm 2014-03-04 11:00:06 +00:00
Mark Steve Samson
96a92a73f1 Fix KeyError when -v is not specified in fig rm 2014-03-04 13:13:23 +08:00
Ben Firshman
48e7b4b0a6 Remove Python 3 from Travis
Let's not kid ourselves.
2014-03-03 19:21:27 +00:00
Aanand Prasad
8c16961afd Merge pull request #131 from orchardup/ship-0.3.0
Ship 0.3.0
2014-03-03 18:58:00 +00:00
Ben Firshman
6e9983fc6a Ship 0.3.0 2014-03-03 18:51:03 +00:00
Ben Firshman
bf87f897d7 Merge pull request #130 from orchardup/fix-hanging-recreate
Fix hanging recreate
2014-03-03 18:49:36 +00:00
Aanand Prasad
a00ec9d1f8 Fix: image-defined entrypoint not overridden by intermediate container
This was causing recreation to hang.
2014-03-03 18:06:06 +00:00
Aanand Prasad
be1ba818e4 Document link aliases 2014-03-03 18:00:27 +00:00
Ben Firshman
b0ac88fd06 Merge pull request #78 from orchardup/document-docker-version
Document which versions of Docker Fig supports
2014-03-03 17:02:13 +00:00
Aanand Prasad
a68b4e6dde Merge pull request #128 from orchardup/better-error-message-when-service-is-not-dict
Improve error when service is not a dict
2014-03-03 16:40:59 +00:00
Aanand Prasad
348ba0818f Reformat comments in YAML reference for readability 2014-03-03 16:23:52 +00:00
Aanand Prasad
f79e0e588e Reword port warning in YAML reference, remove it from README 2014-03-03 16:22:02 +00:00
Ben Firshman
3e7360c2c6 Improve error when service is not a dict
Fixes #127
2014-03-03 16:21:42 +00:00
Aanand Prasad
e6016bd5b4 Merge pull request #104 from Gazler/documentation/port-strings
Documentation: Include notes on mapping ports
2014-03-03 16:21:31 +00:00
Ben Firshman
343d3bb556 Remove unsupported Docker 0.7.6 from Travis 2014-03-03 15:43:48 +00:00
Ben Firshman
17b9801ec9 Document what version of Docker is supported 2014-03-03 15:43:10 +00:00
Aanand Prasad
e550451c69 Merge pull request #25 from orchardup/ship-binaries
Ship OS X binaries
2014-03-03 15:29:59 +00:00
Ben Firshman
29f7b78deb Add installation instructions for binaries 2014-03-03 15:20:09 +00:00
Ben Firshman
431ce67f85 Add script to build Linux binary 2014-03-03 15:10:02 +00:00
Ben Firshman
ba66c849b5 Use Python base image and run as normal user 2014-03-03 15:10:02 +00:00
Ben Firshman
9550387e39 Add script to build an OS X binary 2014-03-03 15:09:56 +00:00
Gary Rennie
b06d37f885 Documentation: Include notes on mapping ports
When mapping ports as strings there is an issue with the way YAML parses
numbers in the format "xx:yy" where yy is less than 60 - this issue is
now included in the documentation.

This fixes #103
2014-03-03 13:37:15 +00:00
Ben Firshman
bf47aa97b4 Fix tests importing six 2014-03-03 12:25:38 +00:00
Ben Firshman
8e42d6fbb3 Remove six from requirements
It was vendorised in 75c430635b
2014-03-03 12:08:38 +00:00
Aanand Prasad
c07e96cf2b Merge pull request #120 from marksteve/link-name
Add custom link names (Closes #72)
2014-03-03 11:22:57 +00:00
Ben Firshman
c2cd55e010 Merge pull request #113 from orchardup/alternate-fig-file
Alternate fig file can be specified with -f
2014-03-03 11:12:09 +00:00
Ben Firshman
fb31b5fff7 Merge pull request #124 from marksteve/delete-volume
Provide option to remove volumes in `fig rm`
2014-03-03 10:37:40 +00:00
Mark Steve Samson
41bacae171 Provide option to remove volumes in fig rm 2014-03-03 17:55:00 +08:00
Mark Steve Samson
193558a4bc Add link names test 2014-03-03 08:54:47 +08:00
Mark Steve Samson
e38e866626 Fix links related test 2014-03-02 00:30:33 +08:00
Mark Steve Samson
c709251f21 Add custom link names (Closes #72) 2014-03-02 00:17:19 +08:00
Aanand Prasad
9d1383ba26 Alternate fig file can be specified with -f 2014-03-01 11:29:23 +00:00
Aanand Prasad
c66e18c913 Fix Dockerfile reference URL in docs 2014-03-01 11:28:31 +00:00
Ben Firshman
75c430635b Vendorise six.py
Because pyinstaller adds an old version to the path:

http://www.pyinstaller.org/ticket/773
2014-02-28 19:16:32 +00:00
Ben Firshman
b789eca421 Update Orchard description 2014-02-28 19:15:39 +00:00
Ben Firshman
53a5dfe26b Update Orchard description 2014-02-28 18:56:54 +00:00
Ben Firshman
3ed9e16558 Merge pull request #114 from orchardup/refactor-errors
Extract error text into errors.py
2014-02-27 19:16:38 +00:00
Aanand Prasad
ff1496a6a5 Indent string literals 2014-02-26 16:34:45 +00:00
Aanand Prasad
d7c714e1c6 Move "Can't find fig.yml" error into errors.py 2014-02-26 15:44:06 +00:00
Aanand Prasad
d7e2a77907 Refactor connection errors
Makes command.py a lot more readable.
2014-02-26 15:31:14 +00:00
Ben Firshman
07fd01ad46 Exit deploy docs script on Fig error 2014-02-24 18:35:06 +00:00
Ben Firshman
496a1d3bfe Update docker-osx version to 0.8.0 2014-02-24 18:33:38 +00:00
Ben Firshman
0860b7ed73 Merge pull request #111 from teozkr/master
Exclude tests from MANIFEST
2014-02-24 11:56:09 +00:00
Teo Klestrup Röijezon
229b59bd6e remove tests from distribution build 2014-02-23 03:37:31 +01:00
Ben Firshman
05e15e27ef Use sys.exit instead of global 2014-02-19 22:42:21 +00:00
Ben Firshman
51131813a3 Fix broken manifest 2014-02-19 08:52:44 +00:00
Ben Firshman
1327e293f6 Add Dockerfile filename to wordpress guide 2014-02-18 10:56:21 +00:00
Ben Firshman
2edc372f41 Update Wordpress guide so you can edit code 2014-02-18 10:52:24 +00:00
Ben Firshman
d368e2fca9 Ship 0.2.2 2014-02-17 21:37:31 +00:00
Ben Firshman
b9c8e3e057 Fix scale not binding ports 2014-02-17 21:33:05 +00:00
Ben Firshman
5d4210ceb3 Fix tests when there is an image with int tag 2014-02-17 21:32:48 +00:00
Ben Firshman
4a6897ef3b Merge pull request #91 from barnybug/master
Implement topological sort using Cormen/Tarjan algorithm to handle a->b->c dependencies and detect a->b->c->a cycles.
2014-02-12 19:39:08 +00:00
Aanand Prasad
fbff8983e4 Merge pull request #83 from dustinlacewell/print-logs-during-attach
Tell fig up to print logs before attaching
2014-02-12 11:35:24 -08:00
Barnaby Gray
6431d52a2e Implement topological sort using Cormen/Tarjan algorithm to handle a->b->c dependencies and detect a->b->c->a cycles. 2014-02-12 09:09:55 +00:00
Aanand Prasad
7faba11245 Merge pull request #87 from orchardup/stop-projects-in-reverse-order-to-starting
Stop projects in reverse order to starting
2014-02-09 16:22:05 -08:00
Ben Firshman
4723345473 Stop projects in reverse order to starting 2014-02-09 16:01:13 -08:00
Ben Firshman
b310516ba7 Merge pull request #86 from orchardup/update-to-docker-0.8.0
Test Docker 0.8.0 on Travis
2014-02-08 03:41:03 +00:00
Ben Firshman
049a10136c Test Docker 0.8.0 on Travis 2014-02-07 10:35:18 -08:00
Dustin Lacewell
511a9beede Tell fig up to print logs before attaching 2014-02-05 17:19:18 -08:00
Ben Firshman
a8bc51b229 Merge pull request #80 from orchardup/ship-0.2.1
Ship 0.2.1
2014-02-04 18:17:26 -08:00
Aanand Prasad
6bad9484be Ship 0.2.1 2014-02-04 18:14:19 -08:00
Ben Firshman
d52f73b29a Merge pull request #79 from orchardup/strict-config
Throw an error if you specify an unrecognised option in `fig.yml`
2014-02-04 18:10:42 -08:00
Aanand Prasad
edf8f14ac0 Throw an error if you specify an unrecognised option in fig.yml
Closes #27.
2014-02-04 17:46:04 -08:00
Ben Firshman
9faa7e47ed Lock version of docker-osx on home page 2014-02-04 17:15:14 -08:00
Ben Firshman
f92317c5ee Merge pull request #77 from orchardup/friendlier-connection-error
Friendlier connection error for docker-osx users
2014-02-04 17:04:21 -08:00
Ben Firshman
fea3bc2eb1 Install docker-osx from tag 2014-02-04 15:47:12 -08:00
Aanand Prasad
2b89494405 Fix Ubuntu check - forgot to actually inspect the distro 2014-02-04 15:31:05 -08:00
Aanand Prasad
2bac1c10b0 Show installation instructions if it looks like Docker isn't installed 2014-02-04 15:19:50 -08:00
Aanand Prasad
5126649de4 Friendlier connection error for docker-osx users 2014-02-04 14:42:55 -08:00
Ben Firshman
690a7d6dd7 Add /build to gitignore 2014-02-03 16:01:30 -08:00
Ben Firshman
19c0c9bf06 Specify working docker-osx version in docs 2014-02-03 16:00:01 -08:00
Aanand Prasad
53958cf9df Merge pull request #70 from orchardup/ship-0.2.0
Ship 0.2.0
2014-01-31 02:57:38 -08:00
Ben Firshman
f94ce7767c Ship 0.2.0 2014-01-31 10:52:57 +00:00
Ben Firshman
62607f4f04 Merge pull request #67 from orchardup/link-services-to-themselves
Link services to themselves
2014-01-31 02:46:52 -08:00
Ben Firshman
e4e9f0bc19 Link services to themselves
E.g. `fig run db ...` will be able to access the db service.
2014-01-30 13:11:23 +00:00
Ben Firshman
e0637990aa Document how one-off commands are configured 2014-01-30 10:50:59 +00:00
Ben Firshman
03842f92e8 Document how to see env vars for a service 2014-01-30 10:49:31 +00:00
Ben Firshman
8a2bbeb1eb Merge pull request #52 from cameronmaske/fix-sort-service-dicts
Fix for #48
2014-01-29 15:47:43 -08:00
Ben Firshman
95681246a5 Merge pull request #66 from damianmoore/doc_django_fix_psycopg
Fixed error where Django can't connect to Postgres because psycopg2 is not installed
2014-01-29 15:36:25 -08:00
Damian Moore
80279f71d8 Fixed error where Django can't connect to Postgres because psycopg2 is not installed 2014-01-29 20:01:11 +00:00
Cameron Maske
ae7573b9b8 Fix for #48.
Rework of how services are sorted based on dependencies using a topological sort.
Includes error handling to detect circular/self imports (should raise a DependecyError).
Added in logging to the CLI to log out any DependencyErrors.
Removed the compact module as it is no longer used.
2014-01-29 18:38:25 +00:00
Aanand Prasad
ee880ca7be Fix recreate_containers test 2014-01-29 13:57:28 +00:00
Ben Firshman
21837ac132 Update docker-py
From 2014572941
2014-01-28 14:51:19 +00:00
Ben Firshman
c270c13baa Move command to fig.yml
As done in the getting started guides. Easier to explain like this,
I think.
2014-01-28 14:39:21 +00:00
Ben Firshman
cbdeff99ee Fix broken test on Python 3 2014-01-28 13:45:47 +00:00
Ben Firshman
303e0cfd86 Update docker-py
From b79fee71ce
2014-01-28 13:34:40 +00:00
Ben Firshman
880bc0d8e3 Fix link to running Django example 2014-01-28 12:46:22 +00:00
Ben Firshman
89d2653662 Add links to Dockerfile docs 2014-01-28 12:46:22 +00:00
Ben Firshman
8b75f7c7d3 Merge pull request #58 from mrchrisadams/load_yaml_as_well_as_yml
Support loading in Figfiles with .yaml extension
2014-01-28 03:35:25 -08:00
Ben Firshman
9967d706e9 Simplify getting started guide
and add links off to dockerfile docs
2014-01-28 10:32:55 +00:00
Ben Firshman
d5bc521ab0 Use new envvar format in getting started guide 2014-01-28 10:25:02 +00:00
Ben Firshman
d4f76ba5f9 Add open-docs script 2014-01-28 10:13:15 +00:00
Chris Adams
239da2ef69 Add missing return value for filename check
Oh my, how embarrassing.
2014-01-28 09:51:33 +00:00
Chris Adams
1d18d747a5 Support loading in Figfiles with .yaml extension
Add basic boilerplate text - feel free to change
2014-01-28 00:43:23 +00:00
Ben Firshman
2883b5be6b Add note about postgres image settings 2014-01-28 00:19:38 +00:00
Ben Firshman
e34b433d93 Build docs before deploying 2014-01-28 00:16:29 +00:00
Ben Firshman
b6d0b8468b Fix ordering in dockerfile 2014-01-28 00:08:11 +00:00
Aanand Prasad
5e5bd939be Stop force-pushing to gh-pages 2014-01-27 21:43:37 +00:00
Aanand Prasad
140ccc7d4e Wordpress getting-started guide 2014-01-27 21:43:22 +00:00
Ben Firshman
b4cbcbefa6 Remove project name from envvars in docs 2014-01-27 21:12:14 +00:00
Ben Firshman
9049f4e378 Add IRC to docs 2014-01-27 21:05:16 +00:00
Ben Firshman
0e58c05361 Cache bust docs CSS 2014-01-27 20:36:57 +00:00
Ben Firshman
818516f150 Make logo a link to index 2014-01-27 20:33:41 +00:00
Ben Firshman
2abf25d827 Clarify services line in readme 2014-01-27 20:04:48 +00:00
Ben Firshman
837593e1ab Clarify line about services 2014-01-27 19:38:30 +00:00
Ben Firshman
274728e5fb Update homepage in setup.py 2014-01-27 18:42:00 +00:00
Ben Firshman
51fb2e6149 Clearer introduction in readme 2014-01-27 18:18:53 +00:00
Ben Firshman
cc29b21bfc Clearer introduction 2014-01-27 18:16:57 +00:00
Ben Firshman
00f5c3bb85 Remove magical comments from readme 2014-01-27 18:12:29 +00:00
Ben Firshman
33795912ff Point readme at documentation 2014-01-27 18:11:40 +00:00
Aanand Prasad
92a2deb979 Update homepage title 2014-01-27 18:05:42 +00:00
Aanand Prasad
40fad23e0b Fix Dockerfile in Rails example
was missing libpq-dev
2014-01-27 18:00:47 +00:00
Ben Firshman
6c8472dd67 Make strapline smaller on mobile 2014-01-27 18:00:47 +00:00
Aanand Prasad
a9838e9116 Update environment variable names in reference
Scheme was changed in 3e7e6e7656
2014-01-27 18:00:47 +00:00
Aanand Prasad
01d0e49a1c New website 2014-01-27 18:00:47 +00:00
Aanand Prasad
ddc721ec8a Merge pull request #55 from orchardup/ship-0.1.4
Ship 0.1.4
2014-01-27 09:57:46 -08:00
Ben Firshman
5035a10cbe Ship 0.1.4 2014-01-27 17:57:02 +00:00
Aanand Prasad
fae387168f Merge pull request #54 from orchardup/add-link-alias-without-project-name
Add link alias without project name
2014-01-27 09:43:22 -08:00
Ben Firshman
5d71c33cd7 Only install unittest2 on Python 2.6 2014-01-27 16:20:45 +00:00
Ben Firshman
9a5a021f91 Rewrite introduction 2014-01-27 16:08:00 +00:00
Ben Firshman
3e7e6e7656 Add link alias without project name
REDIS_1_PORT_6379_TCP_ADDR instead of
FIGTEST_REDIS_1_PORT_6379_TCP_ADDR.

Ref #37
2014-01-27 15:32:16 +00:00
Ben Firshman
1bab14213d Update docker-py
From 0a9512d008
2014-01-27 15:29:31 +00:00
Aanand Prasad
db396b81ef Just deploy the _site directory to gh-pages 2014-01-27 12:26:42 +00:00
Aanand Prasad
f60621ee1b Move docs to master branch
- build with script/build-docs
- deploy with script/deploy-docs
2014-01-27 11:51:15 +00:00
Ben Firshman
7c9c55785d Move mock to dev requirements 2014-01-27 10:00:36 +00:00
Ben Firshman
8f8b0bbd16 Revert "Remove Travis badge"
This reverts commit ac90e0e939.
2014-01-26 21:29:59 +00:00
Ben Firshman
ddf6819a75 Only pull ubuntu:latest in tests
Might stop Travis running out of disk space.
2014-01-26 20:37:40 +00:00
Ben Firshman
ee49e7055b Pull ubuntu image for CLI tests 2014-01-26 20:29:05 +00:00
Ben Firshman
ac90e0e939 Remove Travis badge
Travis is running out of disk space.
2014-01-26 19:54:00 +00:00
Ben Firshman
ea93c01dfb Remove intermediate containers in recreate test 2014-01-26 19:48:10 +00:00
Ben Firshman
cf18a3141f Remove images created by tests 2014-01-26 19:37:35 +00:00
Aanand Prasad
2ebec04811 Hide build/PyPi badges on homepage 2014-01-23 12:19:27 +00:00
Aanand Prasad
b43b007b92 Merge pull request #50 from orchardup/ship-0.1.3
Bump version to 0.1.3
2014-01-23 04:02:44 -08:00
Ben Firshman
33aada05a4 Bump version to 0.1.3 2014-01-23 11:58:48 +00:00
Ben Firshman
0bdd8637db Merge pull request #47 from orchardup/fix-split-buffer
Fig bug in split_buffer where input was being discarded
2014-01-22 10:05:43 -08:00
Aanand Prasad
e8472be6d5 Fig bug in split_buffer where input was being discarded
Also, write some tests for it.
2014-01-22 17:44:04 +00:00
Ben Firshman
84667636a2 Merge pull request #46 from orchardup/fix-port-syntax
Fix port syntax
2014-01-22 09:13:54 -08:00
Aanand Prasad
df9f66d437 Allow ports to be specified in '1234/tcp' format 2014-01-22 17:01:10 +00:00
Aanand Prasad
ae67d55bf2 Fix bug where too many '/tcp' suffixes were added to port config 2014-01-22 16:52:42 +00:00
Ben Firshman
18525554ed Add license to setup.py 2014-01-22 14:15:17 +00:00
Ben Firshman
64513e8d6f Verbose nosetests 2014-01-22 14:00:24 +00:00
73 changed files with 2843 additions and 757 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,7 @@
*.egg-info
*.pyc
/build
/dist
/_site
/docs/_site
/docs/.git-gh-pages
fig.spec

View File

@@ -2,17 +2,23 @@ language: python
python:
- '2.6'
- '2.7'
- '3.2'
- '3.3'
matrix:
allow_failures:
- python: '3.2'
- python: '3.3'
install: script/travis-install
env:
global:
- secure: exbot0LTV/0Wic6ElKCrOZmh2ZrieuGwEqfYKf5rVuwu1sLngYRihh+lBL/hTwc79NSu829pbwiWfsQZrXbk/yvaS7avGR0CLDoipyPxlYa2/rfs/o4OdTZqXv0LcFmmd54j5QBMpWU1S+CYOwNkwas57trrvIpPbzWjMtfYzOU=
install:
- pip install .
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- sudo curl -L -o /usr/local/bin/orchard https://github.com/orchardup/go-orchard/releases/download/2.0.5/linux
- sudo chmod +x /usr/local/bin/orchard
before_script:
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID'
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts create $TRAVIS_JOB_ID || false'
script:
- pwd
- env
- sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION"
- nosetests tests/unit
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && script/travis-integration || false'
after_script:
- '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID || false'
deploy:
provider: pypi
user: orchard

View File

@@ -1,6 +1,80 @@
Change log
==========
0.4.0 (2014-04-29)
------------------
- Support Docker 0.9 and 0.10
- Display progress bars correctly when pulling images (no more ski slopes)
- `fig up` now stops all services when any container exits
- Added support for the `privileged` config option in fig.yml (thanks @kvz!)
- Shortened and aligned log prefixes in `fig up` output
- Only containers started with `fig run` link back to their own service
- Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!)
- Error message improvements
0.3.2 (2014-03-05)
------------------
- Added an `--rm` option to `fig run`. (Thanks @marksteve!)
- Added an `expose` option to `fig.yml`.
0.3.1 (2014-03-04)
------------------
- Added contribution instructions. (Thanks @kvz!)
- Fixed `fig rm` throwing an error.
- Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
0.3.0 (2014-03-03)
------------------
- We now ship binaries for OS X and Linux. No more having to install with Pip!
- Add `-f` flag to specify alternate `fig.yml` files
- Add support for custom link names
- Fix a bug where recreating would sometimes hang
- Update docker-py to support Docker 0.8.0.
- Various documentation improvements
- Various error message improvements
Thanks @marksteve, @Gazler and @teozkr!
0.2.2 (2014-02-17)
------------------
- Resolve dependencies using Cormen/Tarjan topological sort
- Fix `fig up` not printing log output
- Stop containers in reverse order to starting
- Fix scale command not binding ports
Thanks to @barnybug and @dustinlacewell for their work on this release.
0.2.1 (2014-02-04)
------------------
- General improvements to error reporting (#77, #79)
0.2.0 (2014-01-31)
------------------
- Link services to themselves so run commands can access the running service. (#67)
- Much better documentation.
- Make service dependency resolution more reliable. (#48)
- Load Fig configurations with a `.yaml` extension. (#58)
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
0.1.4 (2014-01-27)
------------------
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
0.1.3 (2014-01-23)
------------------
- Fix ports sometimes being configured incorrectly. (#46)
- Fix log output sometimes not displaying. (#47)
0.1.2 (2014-01-22)
------------------

30
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,30 @@
# Contributing to Fig
If you're looking contribute to [Fig](http://orchardup.github.io/fig/)
but you're new to the project or maybe even to Python, here are the steps
that should get you started.
1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example.
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
1. Enter the local directory `cd fig`.
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases.
1. All good? Commit and push to GitHub, and submit a pull request.
## Running the test suite
$ script/test
## Building binaries
Linux:
$ script/build-linux
OS X:
$ script/build-osx
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).

View File

@@ -1,9 +1,10 @@
FROM stackbrew/ubuntu:12.04
RUN apt-get update -qq
RUN apt-get install -y python python-pip
FROM orchardup/python:2.7
ADD requirements.txt /code/
WORKDIR /code/
RUN pip install -r requirements.txt
ADD requirements-dev.txt /code/
RUN pip install -r requirements-dev.txt
ADD . /code/
RUN useradd -d /home/user -m -s /bin/bash user
RUN chown -R user /code/
USER user

View File

@@ -2,9 +2,9 @@ include Dockerfile
include LICENSE
include requirements.txt
include requirements-dev.txt
tox.ini
include tox.ini
include *.md
recursive-include tests *
recursive-exclude tests *
global-exclude *.pyc
global-exclude *.pyo
global-exclude *.un~

273
README.md
View File

@@ -1,12 +1,20 @@
Fig
===
[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig)
[![Build Status](https://travis-ci.org/orchardup/fig.svg?branch=master)](https://travis-ci.org/orchardup/fig)
[![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig)
Punctual, lightweight development environments using Docker.
Fast, isolated development environments using Docker.
Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this:
Define your app's environment with Docker so it can be reproduced anywhere:
FROM orchardup/python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD python app.py
Define the services that make up your app so they can be run together in an isolated environment:
```yaml
web:
@@ -14,11 +22,14 @@ web:
links:
- db
ports:
- 8000:8000
- "8000:8000"
- "49100:22"
db:
image: orchardup/postgresql
```
(No more installing Postgres on your laptop!)
Then type `fig up`, and Fig will start and run your entire app:
![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif)
@@ -32,255 +43,7 @@ There are commands to:
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
Installation and documentation
------------------------------
Getting started
---------------
Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
$ chmod +x /usr/local/bin/docker-osx
$ docker-osx shell
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
Next, install Fig:
$ sudo pip install -U fig
(This command also upgrades Fig when we release a new version. If you dont have pip installed, try `brew install python` or `apt-get install python-pip`.)
You'll want to make a directory for the project:
$ mkdir figtest
$ cd figtest
Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis:
```python
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(
host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'),
port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT'))
)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
```
We define our Python dependencies in a file called `requirements.txt`:
flask
redis
And we define how to build this into a Docker image using a file called `Dockerfile`:
FROM stackbrew/ubuntu:13.10
RUN apt-get -qq update
RUN apt-get install -y python python-pip
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
EXPOSE 5000
CMD python app.py
That tells Docker to create an image with Python and Flask installed on it, run the command `python app.py`, and open port 5000 (the port that Flask listens on).
We then define a set of services using `fig.yml`:
web:
build: .
ports:
- 5000:5000
volumes:
- .:/code
links:
- redis
redis:
image: orchardup/redis
This defines two services:
- `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image.
- `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/).
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
$ fig up
Pulling image orchardup/redis...
Building web...
Starting figtest_redis_1...
Starting figtest_web_1...
figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
figtest_web_1 | * Running on http://0.0.0.0:5000/
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!
If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running:
$ fig up -d
Starting figtest_redis_1...
Starting figtest_web_1...
$ fig ps
Name Command State Ports
-------------------------------------------------------------------
figtest_redis_1 /usr/local/bin/run Up
figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service:
$ fig run web env
See `fig --help` other commands that are available.
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
$ fig stop
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com).
Reference
---------
### fig.yml
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
```yaml
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
build: /path/to/build/dir
-- Override the default command.
command: bundle exec thin -p 3000
-- Link to containers in another service (see "Communicating between containers").
links:
- db
- redis
-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).
ports:
- 3000
- 8000:8000
-- Map volumes from the host machine (HOST:CONTAINER).
volumes:
- cache/:/tmp/cache
-- Add environment variables.
environment:
RACK_ENV: development
```
### Commands
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
Run `fig [COMMAND] --help` for full usage.
#### build
Build or rebuild services.
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
#### help
Get help on a command.
#### kill
Force stop service containers.
#### logs
View output from services.
#### ps
List containers.
#### rm
Remove stopped service containers.
#### run
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
Note that this will not start any services that the command's service links to. So if, for example, your one-off command talks to your database, you will need to run `fig up -d db` first.
#### scale
Set number of containers to run for a service.
Numbers are specified in the form `service=num` as arguments.
For example:
$ fig scale web=2 worker=3
#### start
Start existing containers for a service.
#### stop
Stop running containers without removing them. They can be started again with `fig start`.
#### up
Build, (re)create, start and attach to containers for a service.
By default, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them running.
If there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up.
### Environment variables
Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
<b><i>name</i>\_PORT</b><br>
Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp`
<b><i>name</i>\_NAME</b><br>
Fully qualified container name, e.g. `MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1`
[Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
Full documentation is available on [Fig's website](http://orchardup.github.io/fig/).

3
bin/fig Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env python
from fig.cli.main import main
main()

1
docs/.gitignore-gh-pages Normal file
View File

@@ -0,0 +1 @@
/_site

10
docs/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM stackbrew/ubuntu:13.10
RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python
RUN locale-gen en_US.UTF-8
ADD Gemfile /code/
ADD Gemfile.lock /code/
WORKDIR /code
RUN bundle install
ADD . /code
EXPOSE 4000
CMD bundle exec jekyll build

3
docs/Gemfile Normal file
View File

@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'github-pages'

62
docs/Gemfile.lock Normal file
View File

@@ -0,0 +1,62 @@
GEM
remote: https://rubygems.org/
specs:
RedCloth (4.2.9)
blankslate (2.1.2.4)
classifier (1.3.3)
fast-stemmer (>= 1.0.0)
colorator (0.1)
commander (4.1.5)
highline (~> 1.6.11)
fast-stemmer (1.0.2)
ffi (1.9.3)
github-pages (12)
RedCloth (= 4.2.9)
jekyll (= 1.4.2)
kramdown (= 1.2.0)
liquid (= 2.5.4)
maruku (= 0.7.0)
rdiscount (= 2.1.7)
redcarpet (= 2.3.0)
highline (1.6.20)
jekyll (1.4.2)
classifier (~> 1.3)
colorator (~> 0.1)
commander (~> 4.1.3)
liquid (~> 2.5.2)
listen (~> 1.3)
maruku (~> 0.7.0)
pygments.rb (~> 0.5.0)
redcarpet (~> 2.3.0)
safe_yaml (~> 0.9.7)
toml (~> 0.1.0)
kramdown (1.2.0)
liquid (2.5.4)
listen (1.3.1)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
rb-kqueue (>= 0.2)
maruku (0.7.0)
parslet (1.5.0)
blankslate (~> 2.0)
posix-spawn (0.3.8)
pygments.rb (0.5.4)
posix-spawn (~> 0.3.6)
yajl-ruby (~> 1.1.0)
rb-fsevent (0.9.4)
rb-inotify (0.9.3)
ffi (>= 0.5.0)
rb-kqueue (0.2.0)
ffi (>= 0.5.0)
rdiscount (2.1.7)
redcarpet (2.3.0)
safe_yaml (0.9.7)
toml (0.1.0)
parslet (~> 1.5.0)
yajl-ruby (1.1.0)
PLATFORMS
ruby
DEPENDENCIES
github-pages

3
docs/_config.yml Normal file
View File

@@ -0,0 +1,3 @@
markdown: redcarpet
encoding: utf-8

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta charset="utf-8">
<title>{{ page.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='http://fonts.googleapis.com/css?family=Lilita+One|Lato:300,400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/fig.css?{{ site.time | date:'%Y%m%d%U%H%N%S' }}">
</head>
<body>
<div class="container">
<div class="logo mobile-logo">
<a href="index.html">
<img src="img/logo.png">
Fig
</a>
</div>
<div class="content">{{ content }}</div>
<div class="sidebar">
<h1 class="logo">
<a href="index.html">
<img src="img/logo.png">
Fig
</a>
</h1>
<ul class="nav">
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Install</a></li>
<li><a href="rails.html">Get started with Rails</a></li>
<li><a href="django.html">Get started with Django</a></li>
<li><a href="wordpress.html">Get started with Wordpress</a></li>
</ul>
<ul class="nav">
<li>Reference:</li>
<ul>
<li><a href="yml.html">fig.yml</a></li>
<li><a href="cli.html">Commands</a></li>
<li><a href="env.html">Environment variables</a></li>
</ul>
</ul>
<ul class="nav">
<li><a href="https://github.com/orchardup/fig">Fig on GitHub</a></li>
<li><a href="https://twitter.com/orchardup">Follow us on Twitter</a></li>
<li><a href="http://webchat.freenode.net/?channels=%23orchardup&uio=d4">#orchardup on Freenode</a></li>
</ul>
<div class="badges">
<iframe src="http://ghbtns.com/github-btn.html?user=orchardup&amp;repo=fig&amp;type=watch&amp;count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://orchardup.github.io/fig/">Tweet</a>
</div>
</div>
</div>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-43996733-3', 'orchardup.github.io');
ga('send', 'pageview');
</script>
</body>
</html>

81
docs/cli.md Normal file
View File

@@ -0,0 +1,81 @@
---
layout: default
title: Fig CLI reference
---
CLI reference
=============
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
Run `fig [COMMAND] --help` for full usage.
## build
Build or rebuild services.
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
## help
Get help on a command.
## kill
Force stop service containers.
## logs
View output from services.
## ps
List containers.
## rm
Remove stopped service containers.
## run
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
Note that this will not start any services that the command's service links to. So if, for example, your one-off command talks to your database, you will need to run `fig up -d db` first.
One-off commands are started in new containers with the same config as a normal container for that service, so volumes, links, etc will all be created as expected. The only thing different to a normal container is the command will be overridden with the one specified and no ports will be created in case they collide.
Links are also created between one-off commands and the other containers for that service so you can do stuff like this:
$ fig run db /bin/sh -c "psql -h \$DB_1_PORT_5432_TCP_ADDR -U docker"
## scale
Set number of containers to run for a service.
Numbers are specified in the form `service=num` as arguments.
For example:
$ fig scale web=2 worker=3
## start
Start existing containers for a service.
## stop
Stop running containers without removing them. They can be started again with `fig start`.
## up
Build, (re)create, start and attach to containers for a service.
By default, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them running.
If there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up.
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/

7
docs/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

180
docs/css/fig.css Normal file
View File

@@ -0,0 +1,180 @@
body {
padding-top: 20px;
padding-bottom: 60px;
font-family: 'Lato', sans-serif;
font-weight: 300;
font-size: 18px;
color: #362;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Lato', sans-serif;
font-weight: 400;
color: #25594D;
}
h2, h3, h4, h5, h6 {
margin-top: 1.5em;
}
p {
margin: 20px 0;
}
a, a:hover, a:visited {
color: #4D9900;
text-decoration: underline;
}
pre, code {
border: none;
background: #D5E1B4;
}
code, pre code {
color: #484F40;
}
pre {
border-bottom: 2px solid #bec9a1;
font-size: 14px;
}
code {
font-size: 0.84em;
}
pre code {
background: none;
}
img {
max-width: 100%;
}
.container {
margin-left: 0;
}
.logo {
font-family: 'Lilita One', sans-serif;
font-size: 80px;
margin: 20px 0 40px 0;
}
.logo a {
color: #a41211;
text-decoration: none;
}
.logo img {
width: 80px;
vertical-align: -17px;
}
.mobile-logo {
text-align: center;
}
.sidebar {
font-size: 16px;
}
.sidebar a {
color: #a41211;
}
@media (max-width: 767px) {
.sidebar {
text-align: center;
margin-top: 40px;
}
.sidebar .logo {
display: none;
}
}
@media (min-width: 768px) {
.mobile-logo {
display: none;
}
.logo {
margin-top: 40px;
}
.content h1 {
margin: 60px 0 55px 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 280px;
overflow-y: auto;
padding-left: 40px;
border-right: 1px solid #ccc;
}
.content {
margin-left: 320px;
max-width: 650px;
}
}
.nav {
margin: 20px 0;
}
.nav li a {
display: block;
padding: 8px 0;
line-height: 1.2;
text-decoration: none;
}
.nav li a:hover, .nav li a:focus {
text-decoration: underline;
background: none;
}
.nav ul {
padding-left: 20px;
list-style: none;
}
.badges {
margin: 40px 0;
}
a.btn {
background: #25594D;
color: white;
text-transform: uppercase;
text-decoration: none;
}
a.btn:hover {
color: white;
}
.strapline {
font-size: 30px;
}
@media (min-width: 768px) {
.strapline {
font-size: 40px;
display: block;
line-height: 1.2;
margin-top: 25px;
margin-bottom: 35px;
}
}
strong {
font-weight: 700;
}

91
docs/django.md Normal file
View File

@@ -0,0 +1,91 @@
---
layout: default
title: Getting started with Fig and Django
---
Getting started with Fig and Django
===================================
Let's use Fig to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html).
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
FROM orchardup/python:2.7
RUN apt-get update -qq && apt-get install -y python-psycopg2
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/
That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Second, we define our Python dependencies in a file called `requirements.txt`:
Django
Simple enough. Finally, this is all tied together with a file called `fig.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.
db:
image: orchardup/postgresql
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
links:
- db
See the [`fig.yml` reference](http://orchardup.github.io/fig/yml.html) for more information on how it works.
We can now start a Django project using `fig run`:
$ fig run web django-admin.py startproject figexample .
First, Fig will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject figexample .` inside a container using that image.
This will generate a Django app inside the current directory:
$ ls
Dockerfile fig.yml figexample manage.py requirements.txt
First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `figexample/settings.py` to read:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'docker',
'USER': 'docker',
'PASSWORD': 'docker',
'HOST': os.environ.get('DB_1_PORT_5432_TCP_ADDR'),
'PORT': os.environ.get('DB_1_PORT_5432_TCP_PORT'),
}
}
These settings are determined by the [orchardup/postgresql](https://github.com/orchardup/docker-postgresql) Docker image we are using.
Then, run `fig up`:
Recreating myapp_db_1...
Recreating myapp_web_1...
Attaching to myapp_db_1, myapp_web_1
myapp_db_1 |
myapp_db_1 | PostgreSQL stand-alone backend 9.1.11
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: database system is ready to accept connections
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: autovacuum launcher started
myapp_web_1 | Validating models...
myapp_web_1 |
myapp_web_1 | 0 errors found
myapp_web_1 | January 27, 2014 - 12:12:40
myapp_web_1 | Django version 1.6.1, using settings 'figexample.settings'
myapp_web_1 | Starting development server at http://0.0.0.0:8000/
myapp_web_1 | Quit the server with CONTROL-C.
And your Django app should be running at [localhost:8000](http://localhost:8000) (or [localdocker:8000](http://localdocker:8000) if you're using docker-osx).
You can also run management commands with Docker. To set up your database, for example, run `fig up` and in another terminal run:
$ fig run web python manage.py syncdb

31
docs/env.md Normal file
View File

@@ -0,0 +1,31 @@
---
layout: default
title: Fig environment variables reference
---
Environment variables reference
===============================
Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
To see what environment variables are available to a service, run `fig run SERVICE env`.
<b><i>name</i>\_PORT</b><br>
Full URL, e.g. `DB_1_PORT=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
Full URL, e.g. `DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
Container's IP address, e.g. `DB_1_PORT_5432_TCP_ADDR=172.17.0.5`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
Exposed port number, e.g. `DB_1_PORT_5432_TCP_PORT=5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
Protocol (tcp or udp), e.g. `DB_1_PORT_5432_TCP_PROTO=tcp`
<b><i>name</i>\_NAME</b><br>
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
[Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container

8
docs/fig.yml Normal file
View File

@@ -0,0 +1,8 @@
jekyll:
build: .
ports:
- "4000:4000"
volumes:
- .:/code
environment:
- LANG=en_US.UTF-8

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

145
docs/index.md Normal file
View File

@@ -0,0 +1,145 @@
---
layout: default
title: Fig | Fast, isolated development environments using Docker
---
<strong class="strapline">Fast, isolated development environments using Docker.</strong>
Define your app's environment with Docker so it can be reproduced anywhere:
FROM orchardup/python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
Define the services that make up your app so they can be run together in an isolated environment:
```yaml
web:
build: .
command: python app.py
links:
- db
ports:
- "8000:8000"
db:
image: orchardup/postgresql
```
(No more installing Postgres on your laptop!)
Then type `fig up`, and Fig will start and run your entire app:
![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif)
There are commands to:
- start, stop and rebuild services
- view the status of running services
- tail running services' log output
- run a one-off command on a service
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
Quick start
-----------
Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.
First, [install Docker and Fig](install.html).
You'll want to make a directory for the project:
$ mkdir figtest
$ cd figtest
Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis:
```python
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(
host=os.environ.get('REDIS_1_PORT_6379_TCP_ADDR'),
port=int(os.environ.get('REDIS_1_PORT_6379_TCP_PORT'))
)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
```
We define our Python dependencies in a file called `requirements.txt`:
flask
redis
Next, we want to create a Docker image containing all of our app's dependencies. We specify how to build one using a file called `Dockerfile`:
FROM orchardup/python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
We then define a set of services using `fig.yml`:
web:
build: .
command: python app.py
ports:
- "5000:5000"
volumes:
- .:/code
links:
- redis
redis:
image: orchardup/redis
This defines two services:
- `web`, which is built from `Dockerfile` in the current directory. It also says to run the command `python app.py` inside the image, forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image.
- `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/).
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
$ fig up
Pulling image orchardup/redis...
Building web...
Starting figtest_redis_1...
Starting figtest_web_1...
figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
figtest_web_1 | * Running on http://0.0.0.0:5000/
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!
If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running:
$ fig up -d
Starting figtest_redis_1...
Starting figtest_web_1...
$ fig ps
Name Command State Ports
-------------------------------------------------------------------
figtest_redis_1 /usr/local/bin/run Up
figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service:
$ fig run web env
See `fig --help` other commands that are available.
If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them:
$ fig stop
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com).

31
docs/install.md Normal file
View File

@@ -0,0 +1,31 @@
---
layout: default
title: Installing Fig
---
Installing Fig
==============
First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx
$ chmod +x /usr/local/bin/docker-osx
$ docker-osx shell
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
Next, install Fig. On OS X:
$ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/darwin > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
On 64-bit Linux:
$ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/linux > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
$ sudo pip install -U fig
That should be all you need! Run `fig --version` to see if it worked.

99
docs/rails.md Normal file
View File

@@ -0,0 +1,99 @@
---
layout: default
title: Getting started with Fig and Rails
---
Getting started with Fig and Rails
==================================
We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html).
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
FROM binaryphile/ruby:2.0.0-p247
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
RUN bundle install
ADD . /myapp
That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
source 'https://rubygems.org'
gem 'rails', '4.0.2'
Finally, `fig.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port.
db:
image: orchardup/postgresql
ports:
- "5432"
web:
build: .
command: bundle exec rackup -p 3000
volumes:
- .:/myapp
ports:
- "3000:3000"
links:
- db
With those files in place, we can now generate the Rails skeleton app using `fig run`:
$ fig run web rails new . --force --database=postgresql --skip-bundle
First, Fig will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have a fresh app generated:
$ ls
Dockerfile app fig.yml tmp
Gemfile bin lib vendor
Gemfile.lock config log
README.rdoc config.ru public
Rakefile db test
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've got a Javascript runtime:
gem 'therubyracer', platforms: :ruby
Now that we've got a new `Gemfile`, we need to build the image again. (This, and changes to the Dockerfile itself, should be the only times you'll need to rebuild).
$ fig build
The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on `localhost` - we need to point it at the `db` container instead. We also need to change the username and password to align with the defaults set by `orchardup/postgresql`.
Open up your newly-generated `database.yml`. Replace its contents with the following:
development: &default
adapter: postgresql
encoding: unicode
database: myapp_development
pool: 5
username: docker
password: docker
host: <%= ENV.fetch('DB_1_PORT_5432_TCP_ADDR', 'localhost') %>
port: <%= ENV.fetch('DB_1_PORT_5432_TCP_PORT', '5432') %>
test:
<<: *default
database: myapp_test
We can now boot the app.
$ fig up
If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain:
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu]
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000
Finally, we just need to create the database. In another terminal, run:
$ fig run web rake db:create
And we're rolling—see for yourself at [localhost:3000](http://localhost:3000) (or [localdocker:3000](http://localdocker:3000) if you're using docker-osx).
![Screenshot of Rails' stock index.html](https://orchardup.com/static/images/fig-rails-screenshot.png)

91
docs/wordpress.md Normal file
View File

@@ -0,0 +1,91 @@
---
layout: default
title: Getting started with Fig and Wordpress
---
Getting started with Fig and Wordpress
======================================
Fig makes it nice and easy to run Wordpress in an isolated environment. [Install Fig](install.html), then download Wordpress into the current directory:
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
This will create a directory called `wordpress`, which you can rename to the name of your project if you wish. Inside that directory, we create `Dockerfile`, a file that defines what environment your app is going to run in:
```
FROM orchardup/php5
ADD . /code
```
This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Next up, `fig.yml` starts our web service and a separate MySQL instance:
```
web:
build: .
command: php -S 0.0.0.0:8000 -t /code
ports:
- "8000:8000"
links:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
environment:
MYSQL_DATABASE: wordpress
```
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
```
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', getenv("DB_1_PORT_3306_TCP_ADDR") . ":" . getenv("DB_1_PORT_3306_TCP_PORT"));
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
```
Finally, `router.php` tells PHP's built-in web server how to run Wordpress:
```
<?php
$root = $_SERVER['DOCUMENT_ROOT'];
chdir($root);
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
set_include_path(get_include_path().':'.__DIR__);
if(file_exists($root.$path))
{
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
$path = rtrim($path,'/').'/index.php';
if(strpos($path,'.php') === false) return false;
else {
chdir(dirname($root.$path));
require_once $root.$path;
}
}else include_once 'index.php';
```
With those four files in place, run `fig up` inside your Wordpress directory and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress and set it up by visiting [localhost:8000](http://localhost:8000) - or [localdocker:8000](http://localdocker:8000) if you're using docker-osx.

61
docs/yml.md Normal file
View File

@@ -0,0 +1,61 @@
---
layout: default
title: fig.yml reference
---
fig.yml reference
=================
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
```yaml
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull
-- if it doesn't exist locally.
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
-- Path to a directory containing a Dockerfile. Fig will build and tag it with
-- a generated name, and use that image thereafter.
build: /path/to/build/dir
-- Override the default command.
command: bundle exec thin -p 3000
-- Link to containers in another service. Optionally specify an alternate name
-- for the link, which will determine how environment variables are prefixed,
-- e.g. "db" -> DB_1_PORT, "db:database" -> DATABASE_1_PORT
links:
- db
- db:database
- redis
-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the
-- container port (a random host port will be chosen).
-- Note: When mapping ports in the HOST:CONTAINER format, you may experience
-- erroneous results when using a container port lower than 60, because YAML
-- will parse numbers in the format "xx:yy" as sexagesimal (base 60). For
-- this reason, we recommend always explicitly specifying your port mappings
-- as strings.
ports:
- "3000"
- "8000:8000"
- "49100:22"
-- Expose ports without publishing them to the host machine - they'll only be
-- accessible to linked services. Only the internal port can be specified.
expose:
- "3000"
- "8000"
-- Map volumes from the host machine (HOST:CONTAINER).
volumes:
- cache/:/tmp/cache
-- Add environment variables.
environment:
RACK_ENV: development
```

View File

@@ -1,4 +1,4 @@
from __future__ import unicode_literals
from .service import Service
__version__ = '0.1.2'
__version__ = '0.4.0'

View File

@@ -7,27 +7,44 @@ import logging
import os
import re
import yaml
from ..packages import six
import sys
from ..project import Project
from ..service import ConfigError
from .docopt_command import DocoptCommand
from .formatter import Formatter
from .utils import cached_property, docker_url
from .errors import UserError
from .utils import cached_property, docker_url, call_silently, is_mac, is_ubuntu
from . import errors
log = logging.getLogger(__name__)
class Command(DocoptCommand):
base_dir = '.'
def __init__(self):
self.yaml_path = os.environ.get('FIG_FILE', None)
def dispatch(self, *args, **kwargs):
try:
super(Command, self).dispatch(*args, **kwargs)
except ConnectionError:
raise UserError("""
Couldn't connect to Docker daemon at %s - is it running?
if call_silently(['which', 'docker']) != 0:
if is_mac():
raise errors.DockerNotFoundMac()
elif is_ubuntu():
raise errors.DockerNotFoundUbuntu()
else:
raise errors.DockerNotFoundGeneric()
elif call_silently(['which', 'docker-osx']) == 0:
raise errors.ConnectionErrorDockerOSX()
else:
raise errors.ConnectionErrorGeneric(self.client.base_url)
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
""" % self.client.base_url)
def perform_command(self, options, *args, **kwargs):
if options['--file'] is not None:
self.yaml_path = os.path.join(self.base_dir, options['--file'])
return super(Command, self).perform_command(options, *args, **kwargs)
@cached_property
def client(self):
@@ -36,17 +53,19 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
@cached_property
def project(self):
try:
yaml_path = os.path.join(self.base_dir, 'fig.yml')
yaml_path = self.yaml_path
if yaml_path is None:
yaml_path = self.check_yaml_filename()
config = yaml.load(open(yaml_path))
except IOError as e:
if e.errno == errno.ENOENT:
log.error("Can't find %s. Are you in the right directory?", os.path.basename(e.filename))
else:
log.error(e)
raise errors.FigFileNotFound(os.path.basename(e.filename))
raise errors.UserError(six.text_type(e))
exit(1)
return Project.from_config(self.project_name, config, self.client)
try:
return Project.from_config(self.project_name, config, self.client)
except ConfigError as e:
raise errors.UserError(six.text_type(e))
@cached_property
def project_name(self):
@@ -60,3 +79,12 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
def formatter(self):
return Formatter()
def check_yaml_filename(self):
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
log.warning("Fig just read the file 'fig.yaml' on startup, rather than 'fig.yml'")
log.warning("Please be aware that fig.yml the expected extension in most cases, and using .yaml can cause compatibility issues in future")
return os.path.join(self.base_dir, 'fig.yaml')
else:
return os.path.join(self.base_dir, 'fig.yml')

View File

@@ -8,3 +8,53 @@ class UserError(Exception):
def __unicode__(self):
return self.msg
class DockerNotFoundMac(UserError):
def __init__(self):
super(DockerNotFoundMac, self).__init__("""
Couldn't connect to Docker daemon. You might need to install docker-osx:
https://github.com/noplay/docker-osx
""")
class DockerNotFoundUbuntu(UserError):
def __init__(self):
super(DockerNotFoundUbuntu, self).__init__("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/ubuntulinux/
""")
class DockerNotFoundGeneric(UserError):
def __init__(self):
super(DockerNotFoundGeneric, self).__init__("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/
""")
class ConnectionErrorDockerOSX(UserError):
def __init__(self):
super(ConnectionErrorDockerOSX, self).__init__("""
Couldn't connect to Docker daemon - you might need to run `docker-osx shell`.
""")
class ConnectionErrorGeneric(UserError):
def __init__(self, url):
super(ConnectionErrorGeneric, self).__init__("""
Couldn't connect to Docker daemon at %s - is it running?
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
""" % url)
class FigFileNotFound(UserError):
def __init__(self, filename):
super(FigFileNotFound, self).__init__("""
Can't find %s. Are you in the right directory?
""" % filename)

View File

@@ -4,20 +4,35 @@ import sys
from itertools import cycle
from .multiplexer import Multiplexer
from .multiplexer import Multiplexer, STOP
from . import colors
from .utils import split_buffer
class LogPrinter(object):
def __init__(self, containers, attach_params=None):
self.containers = containers
self.attach_params = attach_params or {}
self.prefix_width = self._calculate_prefix_width(containers)
self.generators = self._make_log_generators()
def run(self):
mux = Multiplexer(self.generators)
for line in mux.loop():
sys.stdout.write(line)
sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8'))
def _calculate_prefix_width(self, containers):
"""
Calculate the maximum width of container names so we can make the log
prefixes line up like so:
db_1 | Listening
web_1 | Listening
"""
prefix_width = 0
for container in containers:
prefix_width = max(prefix_width, len(container.name_without_project))
return prefix_width
def _make_log_generators(self):
color_fns = cycle(colors.rainbow())
@@ -30,10 +45,24 @@ class LogPrinter(object):
return generators
def _make_log_generator(self, container, color_fn):
prefix = color_fn(container.name + " | ")
prefix = color_fn(self._generate_prefix(container))
# Attach to container before log printer starts running
line_generator = split_buffer(self._attach(container), '\n')
return (prefix + line for line in line_generator)
for line in line_generator:
yield prefix + line.decode('utf-8')
exit_code = container.wait()
yield color_fn("%s exited with code %s\n" % (container.name, exit_code))
yield STOP
def _generate_prefix(self, container):
"""
Generate the prefix for a log line without colour
"""
name = container.name_without_project
padding = ' ' * (self.prefix_width - len(name))
return ''.join([name, padding, ' | '])
def _attach(self, container):
params = {
@@ -44,21 +73,3 @@ class LogPrinter(object):
params.update(self.attach_params)
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
return container.attach(**params)
def split_buffer(reader, separator):
"""
Given a generator which yields strings and a separator string,
joins all input, splits on the separator and yields each chunk.
Requires that each input string is decodable as UTF-8.
"""
buffered = ''
for data in reader:
lines = (buffered + data.decode('utf-8')).split(separator)
for line in lines[:-1]:
yield line + separator
if len(lines) > 1:
buffered = lines[-1]
if len(buffered) > 0:
yield buffered

View File

@@ -8,14 +8,14 @@ import signal
from inspect import getdoc
from .. import __version__
from ..project import NoSuchService
from ..service import CannotBeScaledError
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError
from .command import Command
from .formatter import Formatter
from .log_printer import LogPrinter
from .utils import yesno
from ..packages.docker.client import APIError
from ..packages.docker.errors import APIError
from .errors import UserError
from .docopt_command import NoSuchCommand
from .socketclient import SocketClient
@@ -39,21 +39,21 @@ def main():
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except UserError as e:
sys.exit(1)
except (UserError, NoSuchService, ConfigurationError) as e:
log.error(e.msg)
exit(1)
except NoSuchService as e:
log.error(e.msg)
exit(1)
sys.exit(1)
except NoSuchCommand as e:
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
exit(1)
sys.exit(1)
except APIError as e:
log.error(e.explanation)
exit(1)
sys.exit(1)
except BuildError as e:
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
sys.exit(1)
# stolen from docopt master
@@ -73,6 +73,7 @@ class TopLevelCommand(Command):
Options:
--verbose Show more output
--version Print version and exit
-f, --file FILE Specify an alternate fig file (default: fig.yml)
Commands:
build Build or rebuild services
@@ -172,15 +173,23 @@ class TopLevelCommand(Command):
"""
Remove stopped service containers.
Usage: rm [SERVICE...]
Usage: rm [options] [SERVICE...]
Options:
--force Don't ask to confirm removal
-v Remove volumes associated with containers
"""
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
stopped_containers = [c for c in all_containers if not c.is_running]
if len(stopped_containers) > 0:
print("Going to remove", list_containers(stopped_containers))
if yesno("Are you sure? [yN] ", default=False):
self.project.remove_stopped(service_names=options['SERVICE'])
if options.get('--force') \
or yesno("Are you sure? [yN] ", default=False):
self.project.remove_stopped(
service_names=options['SERVICE'],
v=options.get('-v', False)
)
else:
print("No stopped containers")
@@ -203,6 +212,7 @@ class TopLevelCommand(Command):
container name
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
--rm Remove container after run. Ignored in detached mode.
"""
service = self.project.get_service(options['SERVICE'])
@@ -217,12 +227,16 @@ class TopLevelCommand(Command):
}
container = service.create_container(one_off=True, **container_options)
if options['-d']:
service.start_container(container, ports=None)
service.start_container(container, ports=None, one_off=True)
print(container.name)
else:
with self._attach_to_container(container.id, raw=tty) as c:
service.start_container(container, ports=None)
service.start_container(container, ports=None, one_off=True)
c.run()
if options['--rm']:
container.wait()
log.info("Removing %s..." % container.name)
self.client.remove_container(container.id)
def scale(self, options):
"""
@@ -287,20 +301,12 @@ class TopLevelCommand(Command):
"""
detached = options['-d']
(old, new) = self.project.recreate_containers(service_names=options['SERVICE'])
to_attach = self.project.up(service_names=options['SERVICE'])
if not detached:
to_attach = [c for (s, c) in new]
print("Attaching to", list_containers(to_attach))
log_printer = LogPrinter(to_attach)
log_printer = LogPrinter(to_attach, attach_params={"logs": True})
for (service, container) in new:
service.start_container(container)
for (service, container) in old:
container.remove()
if not detached:
try:
log_printer.run()
finally:

View File

@@ -7,6 +7,11 @@ except ImportError:
from queue import Queue, Empty # Python 3.x
# Yield STOP from an input generator to stop the
# top-level loop without processing any more input.
STOP = object()
class Multiplexer(object):
def __init__(self, generators):
self.generators = generators
@@ -17,7 +22,11 @@ class Multiplexer(object):
while True:
try:
yield self.queue.get(timeout=0.1)
item = self.queue.get(timeout=0.1)
if item is STOP:
break
else:
yield item
except Empty:
pass

View File

@@ -81,7 +81,7 @@ class SocketClient:
chunk = socket.recv(4096)
if chunk:
stream.write(chunk)
stream.write(chunk.encode(stream.encoding or 'utf-8'))
stream.flush()
else:
break
@@ -115,7 +115,7 @@ if __name__ == '__main__':
if len(sys.argv) != 2:
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
exit(1)
sys.exit(1)
url = sys.argv[1]
socket = websocket.create_connection(url)

View File

@@ -3,8 +3,8 @@ from __future__ import absolute_import
from __future__ import division
import datetime
import os
import socket
from .errors import UserError
import subprocess
import platform
def cached_property(f):
@@ -83,3 +83,44 @@ def mkdir(path, permissions=0o700):
def docker_url():
return os.environ.get('DOCKER_HOST')
def split_buffer(reader, separator):
"""
Given a generator which yields strings and a separator string,
joins all input, splits on the separator and yields each chunk.
Unlike string.split(), each chunk includes the trailing
separator, except for the last one if none was found on the end
of the input.
"""
buffered = str('')
separator = str(separator)
for data in reader:
buffered += data
while True:
index = buffered.find(separator)
if index == -1:
break
yield buffered[:index+1]
buffered = buffered[index+1:]
if len(buffered) > 0:
yield buffered
def call_silently(*args, **kwargs):
"""
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
"""
with open(os.devnull, 'w') as shutup:
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
def is_mac():
return platform.system() == 'Darwin'
def is_ubuntu():
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'

View File

@@ -1,23 +0,0 @@
# Taken from python2.7/3.3 functools
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
class K(object):
__slots__ = ['obj']
def __init__(self, obj):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
__hash__ = None
return K

View File

@@ -50,6 +50,10 @@ class Container(object):
def name(self):
return self.dictionary['Name'][1:]
@property
def name_without_project(self):
return '_'.join(self.dictionary['Name'].split('_')[1:])
@property
def number(self):
try:
@@ -66,6 +70,8 @@ class Container(object):
for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
if public:
ports.append('%s->%s' % (public[0]['HostPort'], private))
else:
ports.append(private)
return ', '.join(ports)
@property
@@ -82,7 +88,10 @@ class Container(object):
@property
def human_readable_command(self):
self.inspect_if_not_inspected()
return ' '.join(self.dictionary['Config']['Cmd'])
if self.dictionary['Config']['Cmd']:
return ' '.join(self.dictionary['Config']['Cmd'])
else:
return ''
@property
def environment(self):
@@ -107,8 +116,8 @@ class Container(object):
def kill(self):
return self.client.kill(self.id)
def remove(self):
return self.client.remove_container(self.id)
def remove(self, **options):
return self.client.remove_container(self.id, **options)
def inspect_if_not_inspected(self):
if not self.has_been_inspected:

View File

@@ -12,4 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from .client import Client, APIError # flake8: noqa
__title__ = 'docker-py'
__version__ = '0.3.0'
from .client import Client # flake8: noqa

View File

@@ -17,9 +17,10 @@ import fileinput
import json
import os
import six
from fig.packages import six
from ..utils import utils
from .. import errors
INDEX_URL = 'https://index.docker.io/v1/'
DOCKER_CONFIG_FILENAME = '.dockercfg'
@@ -45,18 +46,19 @@ def expand_registry_url(hostname):
def resolve_repository_name(repo_name):
if '://' in repo_name:
raise ValueError('Repository name cannot contain a '
'scheme ({0})'.format(repo_name))
raise errors.InvalidRepository(
'Repository name cannot contain a scheme ({0})'.format(repo_name))
parts = repo_name.split('/', 1)
if not '.' in parts[0] and not ':' in parts[0] and parts[0] != 'localhost':
if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost':
# This is a docker index repo (ex: foo/bar or ubuntu)
return INDEX_URL, repo_name
if len(parts) < 2:
raise ValueError('Invalid repository name ({0})'.format(repo_name))
raise errors.InvalidRepository(
'Invalid repository name ({0})'.format(repo_name))
if 'index.docker.io' in parts[0]:
raise ValueError('Invalid repository name,'
'try "{0}" instead'.format(parts[1]))
raise errors.InvalidRepository(
'Invalid repository name, try "{0}" instead'.format(parts[1]))
return expand_registry_url(parts[0]), parts[1]
@@ -87,6 +89,11 @@ def resolve_authconfig(authconfig, registry=None):
return authconfig.get(swap_protocol(registry), None)
def encode_auth(auth_info):
return base64.b64encode(auth_info.get('username', '') + b':' +
auth_info.get('password', ''))
def decode_auth(auth):
if isinstance(auth, six.string_types):
auth = auth.encode('ascii')
@@ -100,6 +107,12 @@ def encode_header(auth):
return base64.b64encode(auth_json)
def encode_full_header(auth):
""" Returns the given auth block encoded for the X-Registry-Config header.
"""
return encode_header({'configs': auth})
def load_config(root=None):
"""Loads authentication data from a Docker configuration file in the given
root directory."""
@@ -136,7 +149,8 @@ def load_config(root=None):
data.append(line.strip().split(' = ')[1])
if len(data) < 2:
# Not enough data
raise Exception('Invalid or empty configuration file!')
raise errors.InvalidConfigFile(
'Invalid or empty configuration file!')
username, password = decode_auth(data[0])
conf[INDEX_URL] = {

View File

@@ -19,59 +19,31 @@ import struct
import requests
import requests.exceptions
import six
from fig.packages import six
from .auth import auth
from .unixconn import unixconn
from .utils import utils
from . import errors
if not six.PY3:
import websocket
DEFAULT_DOCKER_API_VERSION = '1.9'
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
class APIError(requests.exceptions.HTTPError):
def __init__(self, message, response, explanation=None):
super(APIError, self).__init__(message, response=response)
self.explanation = explanation
if self.explanation is None and response.content:
self.explanation = response.content.strip()
def __str__(self):
message = super(APIError, self).__str__()
if self.is_client_error():
message = '%s Client Error: %s' % (
self.response.status_code, self.response.reason)
elif self.is_server_error():
message = '%s Server Error: %s' % (
self.response.status_code, self.response.reason)
if self.explanation:
message = '%s ("%s")' % (message, self.explanation)
return message
def is_client_error(self):
return 400 <= self.response.status_code < 500
def is_server_error(self):
return 500 <= self.response.status_code < 600
class Client(requests.Session):
def __init__(self, base_url=None, version="1.6",
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
timeout=DEFAULT_TIMEOUT_SECONDS):
super(Client, self).__init__()
if base_url is None:
base_url = "unix://var/run/docker.sock"
if base_url.startswith('unix:///'):
base_url = "http+unix://var/run/docker.sock"
if 'unix:///' in base_url:
base_url = base_url.replace('unix:/', 'unix:')
if base_url.startswith('unix:'):
base_url = "http+" + base_url
if base_url.startswith('tcp:'):
base_url = base_url.replace('tcp:', 'http:')
if base_url.endswith('/'):
@@ -81,7 +53,7 @@ class Client(requests.Session):
self._timeout = timeout
self._auth_configs = auth.load_config()
self.mount('unix://', unixconn.UnixAdapter(base_url, timeout))
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
@@ -106,7 +78,7 @@ class Client(requests.Session):
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise APIError(e, response, explanation=explanation)
raise errors.APIError(e, response, explanation=explanation)
def _result(self, response, json=False, binary=False):
assert not (json and binary)
@@ -123,7 +95,7 @@ class Client(requests.Session):
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, entrypoint=None,
cpu_shares=None, working_dir=None):
cpu_shares=None, working_dir=None, domainname=None):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
@@ -131,7 +103,7 @@ class Client(requests.Session):
'{0}={1}'.format(k, v) for k, v in environment.items()
]
if ports and isinstance(ports, list):
if isinstance(ports, list):
exposed_ports = {}
for port_definition in ports:
port = port_definition
@@ -143,12 +115,15 @@ class Client(requests.Session):
exposed_ports['{0}/{1}'.format(port, proto)] = {}
ports = exposed_ports
if volumes and isinstance(volumes, list):
if isinstance(volumes, list):
volumes_dict = {}
for vol in volumes:
volumes_dict[vol] = {}
volumes = volumes_dict
if volumes_from and not isinstance(volumes_from, six.string_types):
volumes_from = ','.join(volumes_from)
attach_stdin = False
attach_stdout = False
attach_stderr = False
@@ -163,26 +138,27 @@ class Client(requests.Session):
stdin_once = True
return {
'Hostname': hostname,
'Hostname': hostname,
'Domainname': domainname,
'ExposedPorts': ports,
'User': user,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,
'Memory': mem_limit,
'AttachStdin': attach_stdin,
'User': user,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,
'Memory': mem_limit,
'AttachStdin': attach_stdin,
'AttachStdout': attach_stdout,
'AttachStderr': attach_stderr,
'Env': environment,
'Cmd': command,
'Dns': dns,
'Image': image,
'Volumes': volumes,
'VolumesFrom': volumes_from,
'Env': environment,
'Cmd': command,
'Dns': dns,
'Image': image,
'Volumes': volumes,
'VolumesFrom': volumes_from,
'NetworkDisabled': network_disabled,
'Entrypoint': entrypoint,
'CpuShares': cpu_shares,
'WorkingDir': working_dir
'Entrypoint': entrypoint,
'CpuShares': cpu_shares,
'WorkingDir': working_dir
}
def _post_json(self, url, data, **kwargs):
@@ -220,25 +196,26 @@ class Client(requests.Session):
def _create_websocket_connection(self, url):
return websocket.create_connection(url)
def _stream_result(self, response):
"""Generator for straight-out, non chunked-encoded HTTP responses."""
def _get_raw_response_socket(self, response):
self._raise_for_status(response)
for line in response.iter_lines(chunk_size=1):
# filter out keep-alive new lines
if line:
yield line + '\n'
def _stream_result_socket(self, response):
self._raise_for_status(response)
return response.raw._fp.fp._sock
if six.PY3:
return response.raw._fp.fp.raw._sock
else:
return response.raw._fp.fp._sock
def _stream_helper(self, response):
"""Generator for data coming from a chunked-encoded HTTP response."""
socket_fp = self._stream_result_socket(response)
socket_fp = self._get_raw_response_socket(response)
socket_fp.setblocking(1)
socket = socket_fp.makefile()
while True:
size = int(socket.readline(), 16)
# Because Docker introduced newlines at the end of chunks in v0.9,
# and only on some API endpoints, we have to cater for both cases.
size_line = socket.readline()
if size_line == '\r\n':
size_line = socket.readline()
size = int(size_line, 16)
if size <= 0:
break
data = socket.readline()
@@ -263,17 +240,20 @@ class Client(requests.Session):
def _multiplexed_socket_stream_helper(self, response):
"""A generator of multiplexed data blocks coming from a response
socket."""
socket = self._stream_result_socket(response)
socket = self._get_raw_response_socket(response)
def recvall(socket, size):
data = ''
blocks = []
while size > 0:
block = socket.recv(size)
if not block:
return None
data += block
blocks.append(block)
size -= len(block)
sep = bytes() if six.PY3 else str()
data = sep.join(blocks)
return data
while True:
@@ -302,9 +282,18 @@ class Client(requests.Session):
u = self._url("/containers/{0}/attach".format(container))
response = self._post(u, params=params, stream=stream)
# Stream multi-plexing was introduced in API v1.6.
# Stream multi-plexing was only introduced in API v1.6. Anything before
# that needs old-style streaming.
if utils.compare_version('1.6', self._version) < 0:
return stream and self._stream_result(response) or \
def stream_result():
self._raise_for_status(response)
for line in response.iter_lines(chunk_size=1,
decode_unicode=True):
# filter out keep-alive new lines
if line:
yield line
return stream_result() if stream else \
self._result(response, binary=True)
return stream and self._multiplexed_socket_stream_helper(response) or \
@@ -317,20 +306,22 @@ class Client(requests.Session):
'stderr': 1,
'stream': 1
}
if ws:
return self._attach_websocket(container, params)
if isinstance(container, dict):
container = container.get('Id')
u = self._url("/containers/{0}/attach".format(container))
return self._stream_result_socket(self.post(
return self._get_raw_response_socket(self.post(
u, None, params=self._attach_params(params), stream=True))
def build(self, path=None, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, stream=False, timeout=None):
remote = context = headers = None
if path is None and fileobj is None:
raise Exception("Either path or fileobj needs to be provided.")
raise TypeError("Either path or fileobj needs to be provided.")
if fileobj is not None:
context = utils.mkbuildcontext(fileobj)
@@ -339,6 +330,9 @@ class Client(requests.Session):
else:
context = utils.tar(path)
if utils.compare_version('1.8', self._version) >= 0:
stream = True
u = self._url('/build')
params = {
't': tag,
@@ -350,6 +344,19 @@ class Client(requests.Session):
if context is not None:
headers = {'Content-Type': 'application/tar'}
if utils.compare_version('1.9', self._version) >= 0:
# If we don't have any auth data so far, try reloading the config
# file one more time in case anything showed up in there.
if not self._auth_configs:
self._auth_configs = auth.load_config()
# Send the full auth configuration (if any exists), since the build
# could use any (or all) of the registries.
if self._auth_configs:
headers['X-Registry-Config'] = auth.encode_full_header(
self._auth_configs
)
response = self._post(
u,
data=context,
@@ -361,8 +368,9 @@ class Client(requests.Session):
if context is not None:
context.close()
if stream:
return self._stream_result(response)
return self._stream_helper(response)
else:
output = self._result(response)
srch = r'Successfully built ([0-9a-f]+)'
@@ -401,6 +409,8 @@ class Client(requests.Session):
return res
def copy(self, container, resource):
if isinstance(container, dict):
container = container.get('Id')
res = self._post_json(
self._url("/containers/{0}/copy".format(container)),
data={"Resource": resource},
@@ -414,12 +424,12 @@ class Client(requests.Session):
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, name=None, entrypoint=None,
cpu_shares=None, working_dir=None):
cpu_shares=None, working_dir=None, domainname=None):
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from, network_disabled,
entrypoint, cpu_shares, working_dir
entrypoint, cpu_shares, working_dir, domainname
)
return self.create_container_from_config(config, name)
@@ -438,21 +448,7 @@ class Client(requests.Session):
format(container))), True)
def events(self):
u = self._url("/events")
socket = self._stream_result_socket(self.get(u, stream=True))
while True:
chunk = socket.recv(4096)
if chunk:
# Messages come in the format of length, data, newline.
length, data = chunk.split("\n", 1)
length = int(length, 16)
if length > len(data):
data += socket.recv(length - len(data))
yield json.loads(data)
else:
break
return self._stream_helper(self.get(self._url('/events'), stream=True))
def export(self, container):
if isinstance(container, dict):
@@ -469,6 +465,8 @@ class Client(requests.Session):
def images(self, name=None, quiet=False, all=False, viz=False):
if viz:
if utils.compare_version('1.7', self._version) >= 0:
raise Exception('Viz output is not supported in API >= 1.7!')
return self._result(self._get(self._url("images/viz")))
params = {
'filter': name,
@@ -616,7 +614,7 @@ class Client(requests.Session):
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# Do not fail here if no authentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg:
@@ -642,7 +640,7 @@ class Client(requests.Session):
self._auth_configs = auth.load_config()
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
# Do not fail here if no atuhentication exists for this specific
# Do not fail here if no authentication exists for this specific
# registry as we can have a readonly pull. Just put the header if
# we can.
if authcfg:
@@ -650,7 +648,7 @@ class Client(requests.Session):
response = self._post_json(u, None, headers=headers, stream=stream)
else:
response = self._post_json(u, authcfg, stream=stream)
response = self._post_json(u, None, stream=stream)
return stream and self._stream_helper(response) \
or self._result(response)
@@ -680,8 +678,8 @@ class Client(requests.Session):
params={'term': term}),
True)
def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
publish_all_ports=False, links=None, privileged=False):
def start(self, container, binds=None, volumes_from=None, port_bindings=None,
lxc_conf=None, publish_all_ports=False, links=None, privileged=False):
if isinstance(container, dict):
container = container.get('Id')
@@ -696,10 +694,19 @@ class Client(requests.Session):
}
if binds:
bind_pairs = [
'{0}:{1}'.format(host, dest) for host, dest in binds.items()
'%s:%s:%s' % (
h, d['bind'],
'ro' if 'ro' in d and d['ro'] else 'rw'
) for h, d in binds.items()
]
start_config['Binds'] = bind_pairs
if volumes_from and not isinstance(volumes_from, six.string_types):
volumes_from = ','.join(volumes_from)
start_config['VolumesFrom'] = volumes_from
if port_bindings:
start_config['PortBindings'] = utils.convert_port_bindings(
port_bindings
@@ -708,8 +715,11 @@ class Client(requests.Session):
start_config['PublishAllPorts'] = publish_all_ports
if links:
if isinstance(links, dict):
links = six.iteritems(links)
formatted_links = [
'{0}:{1}'.format(k, v) for k, v in sorted(six.iteritems(links))
'{0}:{1}'.format(k, v) for k, v in sorted(links)
]
start_config['Links'] = formatted_links

View File

@@ -0,0 +1,61 @@
# Copyright 2014 dotCloud inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
class APIError(requests.exceptions.HTTPError):
def __init__(self, message, response, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 doesn't
super(APIError, self).__init__(message)
self.response = response
self.explanation = explanation
if self.explanation is None and response.content:
self.explanation = response.content.strip()
def __str__(self):
message = super(APIError, self).__str__()
if self.is_client_error():
message = '%s Client Error: %s' % (
self.response.status_code, self.response.reason)
elif self.is_server_error():
message = '%s Server Error: %s' % (
self.response.status_code, self.response.reason)
if self.explanation:
message = '%s ("%s")' % (message, self.explanation)
return message
def is_client_error(self):
return 400 <= self.response.status_code < 500
def is_server_error(self):
return 500 <= self.response.status_code < 600
class DockerException(Exception):
pass
class InvalidRepository(DockerException):
pass
class InvalidConfigFile(DockerException):
pass

View File

@@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from fig.packages import six
if six.PY3:
import http.client as httplib
@@ -36,11 +36,11 @@ class UnixHTTPConnection(httplib.HTTPConnection, object):
def connect(self):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
sock.connect(self.base_url.replace("unix:/", ""))
sock.connect(self.base_url.replace("http+unix:/", ""))
self.sock = sock
def _extract_path(self, url):
#remove the base_url entirely..
# remove the base_url entirely..
return url.replace(self.base_url, "")
def request(self, method, url, **kwargs):

View File

@@ -1,3 +1,3 @@
from .utils import (
compare_version, convert_port_bindings, mkbuildcontext, ping, tar
compare_version, convert_port_bindings, mkbuildcontext, ping, tar, parse_repository_tag
) # flake8: noqa

View File

@@ -15,9 +15,10 @@
import io
import tarfile
import tempfile
from distutils.version import StrictVersion
import requests
import six
from fig.packages import six
def mkbuildcontext(dockerfile):
@@ -51,15 +52,34 @@ def tar(path):
def compare_version(v1, v2):
return float(v2) - float(v1)
"""Compare docker versions
>>> v1 = '1.9'
>>> v2 = '1.10'
>>> compare_version(v1, v2)
1
>>> compare_version(v2, v1)
-1
>>> compare_version(v2, v2)
0
"""
s1 = StrictVersion(v1)
s2 = StrictVersion(v2)
if s1 == s2:
return 0
elif s1 > s2:
return -1
else:
return 1
def ping(url):
try:
res = requests.get(url)
return res.status >= 400
except Exception:
return False
else:
return res.status_code < 400
def _convert_port_binding(binding):
@@ -94,3 +114,15 @@ def convert_port_bindings(port_bindings):
else:
result[key] = [_convert_port_binding(v)]
return result
def parse_repository_tag(repo):
column_index = repo.rfind(':')
if column_index < 0:
return repo, ""
tag = repo[column_index+1:]
slash_index = tag.find('/')
if slash_index < 0:
return repo[:column_index], tag
return repo, ""

404
fig/packages/six.py Normal file
View File

@@ -0,0 +1,404 @@
"""Utilities for writing code that runs on Python 2 and 3"""
# Copyright (c) 2010-2013 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.3.0"
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
integer_types = int,
class_types = type,
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = basestring,
integer_types = (int, long)
class_types = (type, types.ClassType)
text_type = unicode
binary_type = str
if sys.platform.startswith("java"):
# Jython always uses 32 bits.
MAXSIZE = int((1 << 31) - 1)
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1)
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1)
del X
def _add_doc(func, doc):
"""Add documentation to a function."""
func.__doc__ = doc
def _import_module(name):
"""Import module, returning the module after the last dot."""
__import__(name)
return sys.modules[name]
class _LazyDescr(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result)
# This is a bit ugly, but it avoids running this again.
delattr(tp, self.name)
return result
class MovedModule(_LazyDescr):
def __init__(self, name, old, new=None):
super(MovedModule, self).__init__(name)
if PY3:
if new is None:
new = name
self.mod = new
else:
self.mod = old
def _resolve(self):
return _import_module(self.mod)
class MovedAttribute(_LazyDescr):
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
super(MovedAttribute, self).__init__(name)
if PY3:
if new_mod is None:
new_mod = name
self.mod = new_mod
if new_attr is None:
if old_attr is None:
new_attr = name
else:
new_attr = old_attr
self.attr = new_attr
else:
self.mod = old_mod
if old_attr is None:
old_attr = name
self.attr = old_attr
def _resolve(self):
module = _import_module(self.mod)
return getattr(module, self.attr)
class _MovedItems(types.ModuleType):
"""Lazy loading of moved objects"""
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("copyreg", "copy_reg"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
MovedModule("cPickle", "cPickle", "pickle"),
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
"tkinter.colorchooser"),
MovedModule("tkinter_commondialog", "tkCommonDialog",
"tkinter.commondialog"),
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
"tkinter.simpledialog"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
del attr
moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves")
def add_move(move):
"""Add an item to six.moves."""
setattr(_MovedItems, move.name, move)
def remove_move(name):
"""Remove item from six.moves."""
try:
delattr(_MovedItems, name)
except AttributeError:
try:
del moves.__dict__[name]
except KeyError:
raise AttributeError("no such move, %r" % (name,))
if PY3:
_meth_func = "__func__"
_meth_self = "__self__"
_func_closure = "__closure__"
_func_code = "__code__"
_func_defaults = "__defaults__"
_func_globals = "__globals__"
_iterkeys = "keys"
_itervalues = "values"
_iteritems = "items"
_iterlists = "lists"
else:
_meth_func = "im_func"
_meth_self = "im_self"
_func_closure = "func_closure"
_func_code = "func_code"
_func_defaults = "func_defaults"
_func_globals = "func_globals"
_iterkeys = "iterkeys"
_itervalues = "itervalues"
_iteritems = "iteritems"
_iterlists = "iterlists"
try:
advance_iterator = next
except NameError:
def advance_iterator(it):
return it.next()
next = advance_iterator
try:
callable = callable
except NameError:
def callable(obj):
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
if PY3:
def get_unbound_function(unbound):
return unbound
Iterator = object
else:
def get_unbound_function(unbound):
return unbound.im_func
class Iterator(object):
def next(self):
return type(self).__next__(self)
callable = callable
_add_doc(get_unbound_function,
"""Get the function out of a possibly unbound function""")
get_method_function = operator.attrgetter(_meth_func)
get_method_self = operator.attrgetter(_meth_self)
get_function_closure = operator.attrgetter(_func_closure)
get_function_code = operator.attrgetter(_func_code)
get_function_defaults = operator.attrgetter(_func_defaults)
get_function_globals = operator.attrgetter(_func_globals)
def iterkeys(d, **kw):
"""Return an iterator over the keys of a dictionary."""
return iter(getattr(d, _iterkeys)(**kw))
def itervalues(d, **kw):
"""Return an iterator over the values of a dictionary."""
return iter(getattr(d, _itervalues)(**kw))
def iteritems(d, **kw):
"""Return an iterator over the (key, value) pairs of a dictionary."""
return iter(getattr(d, _iteritems)(**kw))
def iterlists(d, **kw):
"""Return an iterator over the (key, [values]) pairs of a dictionary."""
return iter(getattr(d, _iterlists)(**kw))
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
if sys.version_info[1] <= 1:
def int2byte(i):
return bytes((i,))
else:
# This is about 2x faster than the implementation above on 3.2+
int2byte = operator.methodcaller("to_bytes", 1, "big")
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
else:
def b(s):
return s
def u(s):
return unicode(s, "unicode_escape")
int2byte = chr
import StringIO
StringIO = BytesIO = StringIO.StringIO
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
if PY3:
import builtins
exec_ = getattr(builtins, "exec")
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
print_ = getattr(builtins, "print")
del builtins
else:
def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
if _globs_ is None:
frame = sys._getframe(1)
_globs_ = frame.f_globals
if _locs_ is None:
_locs_ = frame.f_locals
del frame
elif _locs_ is None:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
def print_(*args, **kwargs):
"""The new-style print function."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
if kwargs:
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
newline = unicode("\n")
space = unicode(" ")
else:
newline = "\n"
space = " "
if sep is None:
sep = space
if end is None:
end = newline
for i, arg in enumerate(args):
if i:
write(sep)
write(arg)
write(end)
_add_doc(reraise, """Reraise an exception.""")
def with_metaclass(meta, base=object):
"""Create a base class with a metaclass."""
return meta("NewBase", (base,), {})

View File

@@ -2,21 +2,37 @@ from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from .service import Service
from .compat.functools import cmp_to_key
log = logging.getLogger(__name__)
def sort_service_dicts(services):
# Sort in dependency order
def cmp(x, y):
x_deps_y = y['name'] in x.get('links', [])
y_deps_x = x['name'] in y.get('links', [])
if x_deps_y and not y_deps_x:
return 1
elif y_deps_x and not x_deps_y:
return -1
return 0
return sorted(services, key=cmp_to_key(cmp))
# Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:]
temporary_marked = set()
sorted_services = []
get_service_names = lambda links: [link.split(':')[0] for link in links]
def visit(n):
if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])):
raise DependencyError('A service can not link to itself: %s' % n['name'])
else:
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
if n in unmarked:
temporary_marked.add(n['name'])
dependents = [m for m in services if n['name'] in get_service_names(m.get('links', []))]
for m in dependents:
visit(m)
temporary_marked.remove(n['name'])
unmarked.remove(n)
sorted_services.insert(0, n)
while unmarked:
visit(unmarked[-1])
return sorted_services
class Project(object):
"""
@@ -37,8 +53,16 @@ class Project(object):
# Reference links by object
links = []
if 'links' in service_dict:
for service_name in service_dict.get('links', []):
links.append(project.get_service(service_name))
for link in service_dict.get('links', []):
if ':' in link:
service_name, link_name = link.split(':', 1)
else:
service_name, link_name = link, None
try:
links.append((project.get_service(service_name), link_name))
except NoSuchService:
raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
del service_dict['links']
project.services.append(Service(client=client, project=name, links=links, **service_dict))
return project
@@ -47,6 +71,8 @@ class Project(object):
def from_config(cls, name, config, client):
dicts = []
for service_name, service in list(config.items()):
if not isinstance(service, dict):
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.')
service['name'] = service_name
dicts.append(service)
return cls.from_dicts(name, dicts, client)
@@ -79,33 +105,16 @@ class Project(object):
unsorted = [self.get_service(name) for name in service_names]
return [s for s in self.services if s in unsorted]
def recreate_containers(self, service_names=None):
"""
For each service, create or recreate their containers.
Returns a tuple with two lists. The first is a list of
(service, old_container) tuples; the second is a list
of (service, new_container) tuples.
"""
old = []
new = []
for service in self.get_services(service_names):
(s_old, s_new) = service.recreate_containers()
old += [(service, container) for container in s_old]
new += [(service, container) for container in s_new]
return (old, new)
def start(self, service_names=None, **options):
for service in self.get_services(service_names):
service.start(**options)
def stop(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.stop(**options)
def kill(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.kill(**options)
def build(self, service_names=None, **options):
@@ -115,6 +124,15 @@ class Project(object):
else:
log.info('%s uses an image, skipping' % service.name)
def up(self, service_names=None):
new_containers = []
for service in self.get_services(service_names):
for (_, new) in service.recreate_containers():
new_containers.append(new)
return new_containers
def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
service.remove_stopped(**options)
@@ -134,3 +152,15 @@ class NoSuchService(Exception):
def __str__(self):
return self.msg
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class DependencyError(ConfigurationError):
pass

View File

@@ -1,31 +1,58 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from .packages.docker.client import APIError
from .packages.docker.errors import APIError
import logging
import re
import os
import sys
import json
from .container import Container
log = logging.getLogger(__name__)
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged']
DOCKER_CONFIG_HINTS = {
'link' : 'links',
'port' : 'ports',
'privilege' : 'privileged',
'priviliged': 'privileged',
'privilige' : 'privileged',
'volume' : 'volumes',
}
class BuildError(Exception):
pass
def __init__(self, service, reason):
self.service = service
self.reason = reason
class CannotBeScaledError(Exception):
pass
class ConfigError(ValueError):
pass
class Service(object):
def __init__(self, name, client=None, project='default', links=[], **options):
if not re.match('^[a-zA-Z0-9]+$', name):
raise ValueError('Invalid name: %s' % name)
raise ConfigError('Invalid name: %s' % name)
if not re.match('^[a-zA-Z0-9]+$', project):
raise ValueError('Invalid project: %s' % project)
raise ConfigError('Invalid project: %s' % project)
if 'image' in options and 'build' in options:
raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose']
for k in options:
if k not in supported_options:
msg = "Unsupported config option for %s service: '%s'" % (name, k)
if k in DOCKER_CONFIG_HINTS:
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
raise ConfigError(msg)
self.name = name
self.client = client
@@ -61,6 +88,14 @@ class Service(object):
c.kill(**options)
def scale(self, desired_num):
"""
Adjusts the number of containers to the specified number and ensures they are running.
- creates containers until there are at least `desired_num`
- stops containers until there are at most `desired_num` running
- starts containers until there are at least `desired_num` running
- removes all stopped containers
"""
if not self.can_be_scaled():
raise CannotBeScaledError()
@@ -90,9 +125,11 @@ class Service(object):
while len(running_containers) < desired_num:
c = stopped_containers.pop(0)
log.info("Starting %s..." % c.name)
c.start()
self.start_container(c)
running_containers.append(c)
self.remove_stopped()
def remove_stopped(self, **options):
for c in self.containers(stopped=True):
@@ -105,37 +142,37 @@ class Service(object):
Create a container for this service. If the image doesn't exist, attempt to pull
it.
"""
container_options = self._get_container_options(override_options, one_off=one_off)
container_options = self._get_container_create_options(override_options, one_off=one_off)
try:
return Container.create(self.client, **container_options)
except APIError as e:
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
log.info('Pulling image %s...' % container_options['image'])
self.client.pull(container_options['image'])
output = self.client.pull(container_options['image'], stream=True)
stream_output(output, sys.stdout)
return Container.create(self.client, **container_options)
raise
def recreate_containers(self, **override_options):
"""
If a container for this service doesn't exist, create one. If there are
any, stop them and create new ones. Does not remove the old containers.
If a container for this service doesn't exist, create and start one. If there are
any, stop them, create+start new ones, and remove the old containers.
"""
containers = self.containers(stopped=True)
if len(containers) == 0:
log.info("Creating %s..." % self.next_container_name())
return ([], [self.create_container(**override_options)])
container = self.create_container(**override_options)
self.start_container(container)
return [(None, container)]
else:
old_containers = []
new_containers = []
tuples = []
for c in containers:
log.info("Recreating %s..." % c.name)
(old_container, new_container) = self.recreate_container(c, **override_options)
old_containers.append(old_container)
new_containers.append(new_container)
tuples.append(self.recreate_container(c, **override_options))
return (old_containers, new_containers)
return tuples
def recreate_container(self, container, **override_options):
if container.is_running:
@@ -144,21 +181,24 @@ class Service(object):
intermediate_container = Container.create(
self.client,
image=container.image,
command='echo',
volumes_from=container.id,
entrypoint=None
entrypoint=['echo'],
command=[],
)
intermediate_container.start()
intermediate_container.start(volumes_from=container.id)
intermediate_container.wait()
container.remove()
options = dict(override_options)
options['volumes_from'] = intermediate_container.id
new_container = self.create_container(**options)
self.start_container(new_container, volumes_from=intermediate_container.id)
intermediate_container.remove()
return (intermediate_container, new_container)
def start_container(self, container=None, **override_options):
def start_container(self, container=None, volumes_from=None, **override_options):
if container is None:
container = self.create_container(**override_options)
@@ -172,9 +212,10 @@ class Service(object):
port = str(port)
if ':' in port:
external_port, internal_port = port.split(':', 1)
port_bindings[int(internal_port)] = int(external_port)
else:
port_bindings[int(port)] = None
external_port, internal_port = (None, port)
port_bindings[internal_port] = external_port
volume_bindings = {}
@@ -182,12 +223,19 @@ class Service(object):
for volume in options['volumes']:
if ':' in volume:
external_dir, internal_dir = volume.split(':')
volume_bindings[os.path.abspath(external_dir)] = internal_dir
volume_bindings[os.path.abspath(external_dir)] = {
'bind': internal_dir,
'ro': False,
}
privileged = options.get('privileged', False)
container.start(
links=self._get_links(),
links=self._get_links(link_to_self=override_options.get('one_off', False)),
port_bindings=port_bindings,
binds=volume_bindings,
volumes_from=volumes_from,
privileged=privileged,
)
return container
@@ -205,28 +253,35 @@ class Service(object):
else:
return max(numbers) + 1
def _get_links(self):
links = {}
for service in self.links:
def _get_links(self, link_to_self):
links = []
for service, link_name in self.links:
for container in service.containers():
links[container.name] = container.name
if link_name:
links.append((container.name, link_name))
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
if link_to_self:
for container in self.containers():
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
def _get_container_create_options(self, override_options, one_off=False):
container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options)
container_options.update(override_options)
container_options['name'] = self.next_container_name(one_off)
if 'ports' in container_options:
if 'ports' in container_options or 'expose' in self.options:
ports = []
for port in container_options['ports']:
all_ports = container_options.get('ports', []) + self.options.get('expose', [])
for port in all_ports:
port = str(port)
if ':' in port:
port = port.split(':')[-1]
if '/' not in port:
port = "%s/tcp" % port
if '/' in port:
port = tuple(port.split('/'))
ports.append(port)
container_options['ports'] = ports
@@ -238,6 +293,10 @@ class Service(object):
self.build()
container_options['image'] = self._build_tag_name()
# Priviliged is only required for starting containers, not for creating them
if 'privileged' in container_options:
del container_options['privileged']
return container_options
def build(self):
@@ -249,17 +308,21 @@ class Service(object):
stream=True
)
try:
all_events = stream_output(build_output, sys.stdout)
except StreamOutputError, e:
raise BuildError(self, unicode(e))
image_id = None
for line in build_output:
if line:
match = re.search(r'Successfully built ([0-9a-f]+)', line)
for event in all_events:
if 'stream' in event:
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
if match:
image_id = match.group(1)
sys.stdout.write(line)
if image_id is None:
raise BuildError()
raise BuildError(self)
return image_id
@@ -279,6 +342,84 @@ class Service(object):
return True
class StreamOutputError(Exception):
pass
def stream_output(output, stream):
is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno())
all_events = []
lines = {}
diff = 0
for chunk in output:
event = json.loads(chunk)
all_events.append(event)
if 'progress' in event or 'progressDetail' in event:
image_id = event['id']
if image_id in lines:
diff = len(lines) - lines[image_id]
else:
lines[image_id] = len(lines)
stream.write("\n")
diff = 0
if is_terminal:
# move cursor up `diff` rows
stream.write("%c[%dA" % (27, diff))
print_output_event(event, stream, is_terminal)
if 'id' in event and is_terminal:
# move cursor back down
stream.write("%c[%dB" % (27, diff))
stream.flush()
return all_events
def print_output_event(event, stream, is_terminal):
if 'errorDetail' in event:
raise StreamOutputError(event['errorDetail']['message'])
terminator = ''
if is_terminal and 'stream' not in event:
# erase current line
stream.write("%c[2K\r" % 27)
terminator = "\r"
pass
elif 'progressDetail' in event:
return
if 'time' in event:
stream.write("[%s] " % event['time'])
if 'id' in event:
stream.write("%s: " % event['id'])
if 'from' in event:
stream.write("(from %s) " % event['from'])
status = event.get('status', '')
if 'progress' in event:
stream.write("%s %s%s" % (status, event['progress'], terminator))
elif 'progressDetail' in event:
detail = event['progressDetail']
if 'current' in detail:
percentage = float(detail['current']) / float(detail['total']) * 100
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
else:
stream.write('%s%s' % (status, terminator))
elif 'stream' in event:
stream.write("%s%s" % (event['stream'], terminator))
else:
stream.write("%s%s\n" % (status, terminator))
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')

View File

@@ -1,2 +1,4 @@
mock==1.0.1
nose==1.3.0
unittest2==0.5.1
pyinstaller==2.1
unittest2

View File

@@ -1,8 +1,5 @@
requests==1.2.3
websocket-client==0.11.0
docopt==0.6.1
PyYAML==3.10
requests==2.2.1
texttable==0.8.1
# docker requires six==1.3.0
six==1.3.0
mock==1.0.1
websocket-client==0.11.0

5
script/build-docs Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -ex
pushd docs
fig run jekyll jekyll build
popd

5
script/build-linux Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
mkdir -p `pwd`/dist
chmod 777 `pwd`/dist
docker build -t fig .
docker run -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig

7
script/build-osx Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -ex
rm -rf venv
virtualenv venv
venv/bin/pip install pyinstaller==2.1
venv/bin/pip install .
venv/bin/pyinstaller -F bin/fig

30
script/deploy-docs Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -ex
script/build-docs
pushd docs/_site
export GIT_DIR=.git-gh-pages
export GIT_WORK_TREE=.
if [ ! -d "$GIT_DIR" ]; then
git init
fi
if !(git remote | grep origin); then
git remote add origin git@github.com:orchardup/fig.git
fi
git fetch origin
git reset --soft origin/gh-pages
echo ".git-gh-pages" > .gitignore
git add -u
git add .
git commit -m "update" || echo "didn't commit"
git push origin master:gh-pages
popd

2
script/open-docs Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
open file://`pwd`/docs/_site/index.html

View File

@@ -1,23 +0,0 @@
#!/bin/bash
# Exit on first error
set -ex
# Put Python eggs in a writeable directory
export PYTHON_EGG_CACHE="/tmp/.python-eggs"
# Activate correct virtualenv
TRAVIS_PYTHON_VERSION=$1
source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate
env
# Kill background processes on exit
trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT
# Start docker daemon
docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null &
sleep 2
# $init is set by sekexe
cd $(dirname $init)/.. && nosetests

View File

@@ -1,18 +0,0 @@
#!/bin/bash
set -ex
sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -"
sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
sudo apt-get update
echo exit 101 | sudo tee /usr/sbin/policy-rc.d
sudo chmod +x /usr/sbin/policy-rc.d
sudo apt-get install -qy slirp lxc lxc-docker-0.7.5
git clone git://github.com/jpetazzo/sekexe
python setup.py install
pip install -r requirements-dev.txt
if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then
pip install unittest2
fi

10
script/travis-integration Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -ex
# Kill background processes on exit
trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT
export DOCKER_HOST=tcp://localhost:4243
orchard proxy -H $TRAVIS_JOB_ID $DOCKER_HOST &
sleep 2
nosetests -v

View File

@@ -32,9 +32,10 @@ setup(
name='fig',
version=find_version("fig", "__init__.py"),
description='Punctual, lightweight development environments using Docker',
url='https://github.com/orchardup/fig',
url='http://orchardup.github.io/fig/',
author='Orchard Laboratories Ltd.',
author_email='hello@orchardup.com',
license='BSD',
packages=find_packages(),
include_package_data=True,
test_suite='nose.collector',

View File

@@ -0,0 +1,3 @@
definedinyamlnotyml:
image: ubuntu
command: /bin/sleep 300

View File

@@ -0,0 +1,6 @@
simple:
image: ubuntu
command: /bin/sleep 300
another:
image: ubuntu
command: /bin/sleep 300

View File

@@ -0,0 +1,3 @@
yetanother:
image: ubuntu
command: /bin/sleep 300

View File

@@ -1,12 +1,13 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from . import unittest
from .testcases import DockerClientTestCase
from mock import patch
from six import StringIO
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
class CLITestCase(unittest.TestCase):
class CLITestCase(DockerClientTestCase):
def setUp(self):
super(CLITestCase, self).setUp()
self.command = TopLevelCommand()
self.command.base_dir = 'tests/fixtures/simple-figfile'
@@ -14,15 +15,42 @@ class CLITestCase(unittest.TestCase):
self.command.project.kill()
self.command.project.remove_stopped()
def test_help(self):
self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None))
@patch('sys.stdout', new_callable=StringIO)
def test_ps(self, mock_stdout):
self.command.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_ps_default_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['up', '-d'], None)
self.command.dispatch(['ps'], None)
output = mock_stdout.getvalue()
self.assertIn('fig_simple_1', output)
self.assertIn('fig_another_1', output)
self.assertNotIn('fig_yetanother_1', output)
@patch('sys.stdout', new_callable=StringIO)
def test_ps_alternate_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
output = mock_stdout.getvalue()
self.assertNotIn('fig_simple_1', output)
self.assertNotIn('fig_another_1', output)
self.assertIn('fig_yetanother_1', output)
def test_rm(self):
service = self.command.project.get_service('simple')
service.create_container()
service.kill()
self.assertEqual(len(service.containers(stopped=True)), 1)
self.command.dispatch(['rm', '--force'], None)
self.assertEqual(len(service.containers(stopped=True)), 0)
def test_scale(self):
project = self.command.project
@@ -44,4 +72,3 @@ class CLITestCase(unittest.TestCase):
self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']})
self.assertEqual(len(project.get_service('simple').containers()), 0)
self.assertEqual(len(project.get_service('another').containers()), 0)

View File

@@ -0,0 +1,87 @@
from __future__ import unicode_literals
from fig.project import Project, ConfigurationError
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
web_container_1 = web.create_container()
web_container_2 = web.create_container()
db_container = db.create_container()
project.start(service_names=['web'])
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
project.start()
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
project.stop(service_names=['web'], timeout=1)
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
project.kill(service_names=['db'])
self.assertEqual(len(project.containers()), 0)
self.assertEqual(len(project.containers(stopped=True)), 3)
project.remove_stopped(service_names=['web'])
self.assertEqual(len(project.containers(stopped=True)), 1)
project.remove_stopped()
self.assertEqual(len(project.containers(stopped=True)), 0)
def test_project_up(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
self.assertEqual(len(project.containers()), 1)
old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
project.up()
self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0]
self.assertNotEqual(c.id, old_db_id)
self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
project.kill()
project.remove_stopped()
def test_unscale_after_restart(self):
web = self.create_service('web')
project = Project('figtest', [web], self.client)
project.start()
service = project.get_service('web')
service.scale(1)
self.assertEqual(len(service.containers()), 1)
service.scale(3)
self.assertEqual(len(service.containers()), 3)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 3)
service.scale(1)
self.assertEqual(len(service.containers()), 1)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 1)
# does scale=0 ,makes any sense? after recreating at least 1 container is running
service.scale(0)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 1)
project.kill()
project.remove_stopped()

View File

@@ -2,29 +2,10 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from fig import Service
from fig.service import CannotBeScaledError
from fig.packages.docker.errors import APIError
from .testcases import DockerClientTestCase
class ServiceTest(DockerClientTestCase):
def test_name_validations(self):
self.assertRaises(ValueError, lambda: Service(name=''))
self.assertRaises(ValueError, lambda: Service(name=' '))
self.assertRaises(ValueError, lambda: Service(name='/'))
self.assertRaises(ValueError, lambda: Service(name='!'))
self.assertRaises(ValueError, lambda: Service(name='\xe2'))
self.assertRaises(ValueError, lambda: Service(name='_'))
self.assertRaises(ValueError, lambda: Service(name='____'))
self.assertRaises(ValueError, lambda: Service(name='foo_bar'))
self.assertRaises(ValueError, lambda: Service(name='__foo_bar__'))
Service('a')
Service('foo')
def test_project_validation(self):
self.assertRaises(ValueError, lambda: Service(name='foo', project='_'))
Service(name='foo', project='bar')
def test_containers(self):
foo = self.create_service('foo')
bar = self.create_service('bar')
@@ -109,11 +90,24 @@ class ServiceTest(DockerClientTestCase):
service.start_container(container)
self.assertIn('/var/db', container.inspect()['Volumes'])
def test_create_container_with_specified_volume(self):
service = self.create_service('db', volumes=['/tmp:/host-tmp'])
container = service.create_container()
service.start_container(container)
self.assertIn('/host-tmp', container.inspect()['Volumes'])
def test_recreate_containers(self):
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps'])
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['ps'],
command=['ax']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(old_container.dictionary['Config']['Env'], ['FOO=1'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['ax'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
volume_path = old_container.inspect()['Volumes']['/var/db']
@@ -121,22 +115,22 @@ class ServiceTest(DockerClientTestCase):
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
(intermediate, new) = service.recreate_containers()
self.assertEqual(len(intermediate), 1)
self.assertEqual(len(new), 1)
tuples = service.recreate_containers()
self.assertEqual(len(tuples), 1)
new_container = new[0]
intermediate_container = intermediate[0]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None)
intermediate_container = tuples[0][0]
new_container = tuples[0][1]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['echo'])
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['ax'])
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.name, 'figtest_db_1')
service.start_container(new_container)
self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path)
self.assertEqual(len(self.client.containers(all=True)), num_containers_before + 1)
self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
self.assertNotEqual(old_container.id, new_container.id)
self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id))
def test_start_container_passes_through_options(self):
db = self.create_service('db')
@@ -150,12 +144,30 @@ class ServiceTest(DockerClientTestCase):
def test_start_container_creates_links(self):
db = self.create_service('db')
web = self.create_service('web', links=[db])
web = self.create_service('web', links=[(db, None)])
db.start_container()
web.start_container()
self.assertIn('figtest_db_1', web.containers()[0].links())
db.stop(timeout=1)
web.stop(timeout=1)
self.assertIn('db_1', web.containers()[0].links())
def test_start_container_creates_links_with_names(self):
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'custom_link_name')])
db.start_container()
web.start_container()
self.assertIn('custom_link_name', web.containers()[0].links())
def test_start_normal_container_does_not_create_links_to_its_own_service(self):
db = self.create_service('db')
c1 = db.start_container()
c2 = db.start_container()
self.assertNotIn(c1.name, c2.links())
def test_start_one_off_container_creates_links_to_its_own_service(self):
db = self.create_service('db')
c1 = db.start_container()
c2 = db.start_container(one_off=True)
self.assertIn(c1.name, c2.links())
def test_start_container_builds_images(self):
service = Service(
@@ -184,20 +196,40 @@ class ServiceTest(DockerClientTestCase):
def test_start_container_creates_ports(self):
service = self.create_service('web', ports=[8000])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp'])
self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
def test_start_container_stays_unpriviliged(self):
service = self.create_service('web')
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Privileged'], False)
def test_start_container_becomes_priviliged(self):
service = self.create_service('web', privileged = True)
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Privileged'], True)
def test_expose_does_not_publish_ports(self):
service = self.create_service('web', expose=[8000])
container = service.start_container().inspect()
self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None})
def test_start_container_creates_port_with_explicit_protocol(self):
service = self.create_service('web', ports=['8000/udp'])
container = service.start_container().inspect()
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp'])
def test_start_container_creates_fixed_external_ports(self):
service = self.create_service('web', ports=['8000:8000'])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
service = self.create_service('web', ports=['8001:8000'])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8001')
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001')
def test_scale(self):
service = self.create_service('web')
@@ -214,5 +246,10 @@ class ServiceTest(DockerClientTestCase):
service = self.create_service('web', ports=['8000:8000'])
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
def test_scale_sets_ports(self):
service = self.create_service('web', ports=['8000'])
service.scale(2)
containers = service.containers()
self.assertEqual(len(containers), 2)
for container in containers:
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])

View File

@@ -3,28 +3,32 @@ from __future__ import absolute_import
from fig.packages.docker import Client
from fig.service import Service
from fig.cli.utils import docker_url
from . import unittest
from .. import unittest
class DockerClientTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.client = Client(docker_url())
cls.client.pull('ubuntu')
cls.client.pull('ubuntu', tag='latest')
def setUp(self):
for c in self.client.containers(all=True):
if c['Names'] and 'figtest' in c['Names'][0]:
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images():
if isinstance(i.get('Tag'), basestring) and 'figtest' in i['Tag']:
self.client.remove_image(i)
def create_service(self, name, **kwargs):
if 'command' not in kwargs:
kwargs['command'] = ["/bin/sleep", "300"]
return Service(
project='figtest',
name=name,
client=self.client,
image="ubuntu",
command=["/bin/sleep", "300"],
**kwargs
)

View File

@@ -1,95 +0,0 @@
from __future__ import unicode_literals
from fig.project import Project
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
def test_from_dict(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'ubuntu'
},
{
'name': 'db',
'image': 'ubuntu'
}
], self.client)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'ubuntu',
'links': ['db'],
},
{
'name': 'db',
'image': 'ubuntu'
}
], self.client)
self.assertEqual(project.services[0].name, 'db')
self.assertEqual(project.services[1].name, 'web')
def test_get_service(self):
web = self.create_service('web')
project = Project('test', [web], self.client)
self.assertEqual(project.get_service('web'), web)
def test_recreate_containers(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('test', [web, db], self.client)
old_web_container = web.create_container()
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 0)
(old, new) = project.recreate_containers()
self.assertEqual(len(old), 1)
self.assertEqual(old[0][0], web)
self.assertEqual(len(new), 2)
self.assertEqual(new[0][0], web)
self.assertEqual(new[1][0], db)
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 1)
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
web_container_1 = web.create_container()
web_container_2 = web.create_container()
db_container = db.create_container()
project.start(service_names=['web'])
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
project.start()
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
project.stop(service_names=['web'], timeout=1)
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
project.kill(service_names=['db'])
self.assertEqual(len(project.containers()), 0)
self.assertEqual(len(project.containers(stopped=True)), 3)
project.remove_stopped(service_names=['web'])
self.assertEqual(len(project.containers(stopped=True)), 1)
project.remove_stopped()
self.assertEqual(len(project.containers(stopped=True)), 0)

0
tests/unit/__init__.py Normal file
View File

16
tests/unit/cli_test.py Normal file
View File

@@ -0,0 +1,16 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from .. import unittest
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
class CLITestCase(unittest.TestCase):
def test_yaml_filename_check(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-figfile'
self.assertTrue(command.project.get_service('definedinyamlnotyml'))
def test_help(self):
command = TopLevelCommand()
with self.assertRaises(SystemExit):
command.dispatch(['-h'], None)

View File

@@ -1,10 +1,10 @@
from __future__ import unicode_literals
from .testcases import DockerClientTestCase
from .. import unittest
from fig.container import Container
class ContainerTest(DockerClientTestCase):
class ContainerTest(unittest.TestCase):
def test_from_ps(self):
container = Container.from_ps(self.client, {
container = Container.from_ps(None, {
"Id":"abc",
"Image":"ubuntu:12.04",
"Command":"sleep 300",
@@ -13,16 +13,16 @@ class ContainerTest(DockerClientTestCase):
"Ports":None,
"SizeRw":0,
"SizeRootFs":0,
"Names":["/db_1"]
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
self.assertEqual(container.dictionary, {
"ID": "abc",
"Image":"ubuntu:12.04",
"Name": "/db_1",
"Name": "/figtest_db_1",
})
def test_environment(self):
container = Container(self.client, {
container = Container(None, {
'ID': 'abc',
'Config': {
'Env': [
@@ -37,7 +37,7 @@ class ContainerTest(DockerClientTestCase):
})
def test_number(self):
container = Container.from_ps(self.client, {
container = Container.from_ps(None, {
"Id":"abc",
"Image":"ubuntu:12.04",
"Command":"sleep 300",
@@ -46,6 +46,24 @@ class ContainerTest(DockerClientTestCase):
"Ports":None,
"SizeRw":0,
"SizeRootFs":0,
"Names":["/db_1"]
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
self.assertEqual(container.number, 1)
def test_name(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"ubuntu:12.04",
"Command":"sleep 300",
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
self.assertEqual(container.name, "figtest_db_1")
def test_name_without_project(self):
container = Container.from_ps(None, {
"Id":"abc",
"Image":"ubuntu:12.04",
"Command":"sleep 300",
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
self.assertEqual(container.name_without_project, "db_1")

View File

@@ -0,0 +1,69 @@
from __future__ import unicode_literals
from .. import unittest
from fig.service import Service
from fig.project import Project, ConfigurationError
class ProjectTest(unittest.TestCase):
def test_from_dict(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'ubuntu'
},
{
'name': 'db',
'image': 'ubuntu'
}
], None)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'ubuntu',
'links': ['db'],
},
{
'name': 'db',
'image': 'ubuntu'
}
], None)
self.assertEqual(project.services[0].name, 'db')
self.assertEqual(project.services[1].name, 'web')
def test_from_config(self):
project = Project.from_config('figtest', {
'web': {
'image': 'ubuntu',
},
'db': {
'image': 'ubuntu',
},
}, None)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
def test_from_config_throws_error_when_not_dict(self):
with self.assertRaises(ConfigurationError):
project = Project.from_config('figtest', {
'web': 'ubuntu',
}, None)
def test_get_service(self):
web = Service(
project='figtest',
name='web',
client=None,
image="ubuntu",
)
project = Project('test', [web], None)
self.assertEqual(project.get_service('web'), web)

View File

@@ -0,0 +1,29 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from .. import unittest
from fig import Service
from fig.service import ConfigError
class ServiceTest(unittest.TestCase):
def test_name_validations(self):
self.assertRaises(ConfigError, lambda: Service(name=''))
self.assertRaises(ConfigError, lambda: Service(name=' '))
self.assertRaises(ConfigError, lambda: Service(name='/'))
self.assertRaises(ConfigError, lambda: Service(name='!'))
self.assertRaises(ConfigError, lambda: Service(name='\xe2'))
self.assertRaises(ConfigError, lambda: Service(name='_'))
self.assertRaises(ConfigError, lambda: Service(name='____'))
self.assertRaises(ConfigError, lambda: Service(name='foo_bar'))
self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__'))
Service('a')
Service('foo')
def test_project_validation(self):
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_'))
Service(name='foo', project='bar')
def test_config_validation(self):
self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000']))
Service(name='foo', ports=['8000'])

View File

@@ -0,0 +1,148 @@
from fig.project import sort_service_dicts, DependencyError
from .. import unittest
class SortServiceTest(unittest.TestCase):
def test_sort_service_dicts_1(self):
services = [
{
'links': ['redis'],
'name': 'web'
},
{
'name': 'grunt'
},
{
'name': 'redis'
}
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'grunt')
self.assertEqual(sorted_services[1]['name'], 'redis')
self.assertEqual(sorted_services[2]['name'], 'web')
def test_sort_service_dicts_2(self):
services = [
{
'links': ['redis', 'postgres'],
'name': 'web'
},
{
'name': 'postgres',
'links': ['redis']
},
{
'name': 'redis'
}
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'redis')
self.assertEqual(sorted_services[1]['name'], 'postgres')
self.assertEqual(sorted_services[2]['name'], 'web')
def test_sort_service_dicts_3(self):
services = [
{
'name': 'child'
},
{
'name': 'parent',
'links': ['child']
},
{
'links': ['parent'],
'name': 'grandparent'
},
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'child')
self.assertEqual(sorted_services[1]['name'], 'parent')
self.assertEqual(sorted_services[2]['name'], 'grandparent')
def test_sort_service_dicts_circular_imports(self):
services = [
{
'links': ['redis'],
'name': 'web'
},
{
'name': 'redis',
'links': ['web']
},
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('redis', e.msg)
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_circular_imports_2(self):
services = [
{
'links': ['postgres', 'redis'],
'name': 'web'
},
{
'name': 'redis',
'links': ['web']
},
{
'name': 'postgres'
}
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('redis', e.msg)
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_circular_imports_3(self):
services = [
{
'links': ['b'],
'name': 'a'
},
{
'name': 'b',
'links': ['c']
},
{
'name': 'c',
'links': ['a']
}
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('a', e.msg)
self.assertIn('b', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_self_imports(self):
services = [
{
'links': ['web'],
'name': 'web'
},
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')

View File

@@ -0,0 +1,37 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from fig.cli.utils import split_buffer
from .. import unittest
class SplitBufferTest(unittest.TestCase):
def test_single_line_chunks(self):
def reader():
yield "abc\n"
yield "def\n"
yield "ghi\n"
self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi\n"])
def test_no_end_separator(self):
def reader():
yield "abc\n"
yield "def\n"
yield "ghi"
self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"])
def test_multiple_line_chunk(self):
def reader():
yield "abc\ndef\nghi"
self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"])
def test_chunked_line(self):
def reader():
yield "a"
yield "b"
yield "c"
yield "\n"
yield "d"
self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "d"])