Compare commits

..

656 Commits
0.0.1 ... 1.0.0

Author SHA1 Message Date
Aanand Prasad
6580c5609c Merge pull request #526 from bfirsh/ship-1.0.0
WIP: Ship 1.0.0
2014-10-16 18:54:56 +01:00
Ben Firshman
a07c83659d Merge pull request #537 from aanand/tls
TLS support
2014-10-16 18:53:59 +01:00
Aanand Prasad
0f58b9f6b4 Update docker-py to 0.5.3
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-16 18:46:55 +01:00
Aanand Prasad
fed391a23e Update documentation for TLS and boot2docker
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-16 18:46:48 +01:00
Ben Firshman
4d1b2f1547 Ship 1.0.0
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-16 16:27:13 +01:00
Aanand Prasad
60411e9f05 Vendor dockerpty at c8b493553477c9a57d163c71c97b2102f44a6ce7
Include TLS fixes and relative imports:

c8b4935534

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-16 16:24:39 +01:00
Aanand Prasad
b318585f3c TLS support, with same env vars as docker client
Thanks to @jkingyens for the bulk of the work.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-16 10:41:26 +01:00
Ben Firshman
1820306d0a Merge pull request #529 from chmouel/override-environement
Allow overriding environements on command line
2014-10-14 11:27:20 +01:00
Aanand Prasad
bf1c1b4c17 Merge pull request #534 from bfirsh/use-bin-echo-for-intermediate-container
Use /bin/echo for intermediate container
2014-10-10 13:12:33 +01:00
Ben Firshman
352062c2dc Use /bin/echo for intermediate container
In cases where the service is using a minimal container,
/bin/echo can be created but echo cannot.

See #517

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-10 12:24:00 +01:00
Chmouel Boudjnah
92249364b6 Allow overriding environements on command line
Add a new command line option -e to override environement variables when
running a service.

Signed-off-by: Chmouel Boudjnah <chmouel@chmouel.com>
2014-10-09 16:33:41 +02:00
Ben Firshman
872a1b5a5c Merge pull request #523 from aanand/remove-references-to-docker-osx
Remove references to docker-osx
2014-10-06 16:05:06 +01:00
Aanand Prasad
b969988ccb Remove references to docker-osx
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-06 14:21:49 +01:00
Aanand Prasad
a5aac7d59e Merge pull request #508 from bfirsh/recommend-boot2docker-in-install-instructions
Recommend boot2docker in installation instructions
2014-10-06 13:44:23 +01:00
Ben Firshman
c16f4d4041 Recommend boot2docker in installation instructions
Fixes #26

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-06 13:14:12 +01:00
Aanand Prasad
d91c458d52 Merge pull request #509 from bfirsh/smarter-binary-urls
Use uname to generate binary download URL
2014-10-06 12:35:36 +01:00
Ben Firshman
475a7e19a9 Merge pull request #521 from aanand/clean-up-env-docs
Clean up environment variable docs
2014-10-06 12:34:35 +01:00
Aanand Prasad
f43bfaadaa Clean up environment variable docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-06 11:58:16 +01:00
Aanand Prasad
2680756dd6 Merge pull request #518 from dnephin/env_docs
Add environment variables to cli.md docs
2014-10-06 11:36:21 +01:00
Daniel Nephin
837f368361 Add environment variables to cli.md docs.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-10-03 13:47:54 -04:00
Aanand Prasad
9df5481066 Merge pull request #512 from bfirsh/fix-entrypoint
Fix fig run entrypoint option
2014-10-01 15:39:00 -07:00
Aanand Prasad
59c7528b4e Merge pull request #513 from bfirsh/update-wercker-badge
Update wercker badge
2014-10-01 14:57:47 -07:00
Ben Firshman
d2385e3c2c Fix fig run entrypoint option
Slipped through because Wercker didn't report build status.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-01 14:19:23 -07:00
Ben Firshman
6b600faf0b Update wercker badge
I moved the project.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-01 12:50:30 -07:00
Aanand Prasad
431fdaa0f1 Merge pull request #490 from LuminosoInsight/insecure-pull
Allow pulls from an insecure registry
2014-10-01 10:52:54 -07:00
Aanand Prasad
ed12f2539c Merge pull request #511 from bfirsh/support_entrypoint
Add support for entrypoint to "fig run"
2014-10-01 10:51:50 -07:00
Ben Firshman
0dc19cb885 Order "fig run" options alphabetically
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-01 09:28:28 -07:00
satoru
62b9c64311 Add support for the --entrypoint option of docker run
Signed-off-by: Satoru Logic <satorulogic@gmail.com>
2014-10-01 09:28:28 -07:00
Moss Collum
3408e0d463 Add unit test for Service.pull
Signed-off-by: Moss Collum <mcollum@luminoso.com>
2014-10-01 11:09:19 -04:00
Ben Firshman
f4b599551a Use uname to generate binary download URL
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-30 18:59:18 -07:00
Aanand Prasad
267be12bb2 Merge pull request #456 from dnephin/volumes_from_service
Fix volumes_from a service with no containers
2014-09-30 18:33:39 -07:00
Aanand Prasad
ec5c864cc7 Merge pull request #460 from dnephin/fix_tests_for_docker_1.1
Fix test failures on docker 1.1.2
2014-09-30 17:52:38 -07:00
Aanand Prasad
cabe47a379 Merge pull request #505 from bfirsh/use-debian-wheezy-base-image
Compile against older version of GLIBC
2014-09-30 17:07:15 -07:00
Ben Firshman
b4fbab4b56 Run pyinstaller build as normal user
... and test build on CI so we don't break it again!

Fixes #503

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-30 16:25:40 -07:00
Alexander Holbreich
6797a322b5 Changing to stable debian (wheezy).
Therefore it compiles agains more common version of GLIBC.

Now works out of the box on Debian wheezy, centos:centos6 and other stable
Of course it works on new distributions. Tested with:
    debian:jessie and
    ubuntu:14.04

Signed-off-by: Alexander Holbreich <alexander@holbreich.org>

Conflicts:
	Dockerfile
2014-09-30 16:25:13 -07:00
Aanand Prasad
e04c5cb52c Merge pull request #506 from bfirsh/make-test-script-use-docker
Make script/test use docker
2014-09-30 15:16:07 -07:00
Ben Firshman
a3f70a9f64 Make script/test use Docker
Really easy to run entire test suite with Docker now. Also switch
Wercker to use the same script.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-30 13:51:42 -07:00
Ben Firshman
f407504679 Remove old Travis files
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-30 13:47:47 -07:00
Aanand Prasad
253b245a1c Merge pull request #492 from dnephin/project_name_from_env
Support setting project name from the environment
2014-09-29 15:32:29 -07:00
Daniel Nephin
fac49b62b6 Support setting project name from the environment.
Signed-off-by: Daniel Nephin <dnephin@yelp.com>
2014-09-29 18:01:08 -04:00
Ben Firshman
92ae5af019 Merge pull request #501 from aanand/fix-build-error
Fix race condition in cli_test.py
2014-09-28 04:54:08 +01:00
Jason Bernardino Alonso
c270e9d622 Require docker-py 0.5 or later for insecure_registry kwarg
Signed-off-by: Jason Bernardino Alonso <jalonso@luminoso.com>
2014-09-26 16:36:42 -04:00
Jason Bernardino Alonso
1c5194e2ec Allow pulls from an insecure registry
Signed-off-by: Jason Bernardino Alonso <jalonso@luminoso.com>
2014-09-26 16:36:36 -04:00
Aanand Prasad
537d435a28 Fix race condition in cli_test.py
We weren't waiting for the build to finish, causing the rest of the test
to occasionally fail when looking for the image.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-09-26 11:42:16 -07:00
Aanand Prasad
5d76d183b4 Merge pull request #483 from dnephin/sort_containers_by_name_in_ps
Sort containers in ps output by name
2014-09-26 10:18:08 -07:00
Aanand Prasad
d4b7ed94e1 Merge pull request #499 from bfirsh/wercker-badge
Add wercker badge to readme
2014-09-25 11:37:03 -07:00
Aanand Prasad
d978787fcc Merge pull request #439 from dnephin/faster_integration_tests
Faster integration testing
2014-09-24 17:34:52 -07:00
Ben Firshman
3535270ef0 Add wercker badge to readme
And removed PyPi badge because who cares.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-24 14:19:27 -07:00
Ben Firshman
b9eb55a225 Merge pull request #498 from bfirsh/mieciu-patch-1
Fix the broken URL
2014-09-24 21:53:57 +01:00
mieciu
35b217a0a4 Fix the broken URL
It was not redirecting to the proper page

Signed-off-by: Przemek Hejman <przemyslaw.hejman@gmail.com>
2014-09-24 13:52:08 -07:00
Aanand Prasad
c37dc558fb Merge pull request #497 from bfirsh/wercker
Add wercker.yml
2014-09-24 10:51:56 -07:00
Ben Firshman
7a8f5e10fd Add wercker.yml
Changed Dockerfile to run as root so it has access to
/var/run/docker.sock.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-23 10:40:51 -07:00
Daniel Nephin
192fce9153 Resolves #43 - sort containers in ps output by name, so services are grouped together.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-14 16:06:23 -04:00
Ben Firshman
fc4c35e977 Merge pull request #411 from Banno/fig-pull
adding "fig pull [SERVICE]" to pull service images
2014-09-10 23:57:20 +01:00
Luke Amdor
648c89768b adding 'fig pull' to cli docs
Signed-off-by: Luke Amdor <luke.amdor@gmail.com>
2014-09-10 16:20:14 -05:00
Daniel Nephin
e0b0801e87 Do less work during integration testing.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-09 22:00:21 -04:00
Daniel Nephin
71e7103662 Fix some tests failing with docker 1.1.2 and add a comment to recreate_container() explaining what it does.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-09 20:59:28 -04:00
Daniel Nephin
dbd723659b Add container.get() which removes the duplication of container.inspect() in every property, and provides a nicer interface for querying container data.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-09 20:59:28 -04:00
Aanand Prasad
6b221d5687 Merge pull request #477 from bfirsh/docker-py-0.5.0
Upgrade to docker-py 0.5.0
2014-09-08 14:20:12 -07:00
Ben Firshman
ce8ef23c09 Merge pull request #393 from marksteve/restart
Implement restart command (Closes #98)
2014-09-08 17:47:08 +01:00
Ben Firshman
b0159e5100 Upgrade to docker-py 0.5.0
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-08 09:23:01 -07:00
Ben Firshman
ee6bb9a252 Merge pull request #475 from bfirsh/fix-missing-six-package
Fix missing six package
2014-09-06 02:20:55 +01:00
Ben Firshman
866050937a Fix missing six package
I had some .pyc files kicking around, urgh.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-09-05 17:19:53 -07:00
Ben Firshman
41ee65b664 Merge branch 'dnephin-replace_packages_with_deps'
Closes #375
2014-09-05 11:55:17 -07:00
Daniel Nephin
7fd37c89b9 Remove fig.packages replace with real deps.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-05 11:44:49 -07:00
Aanand Prasad
8d3c9dccc5 Merge pull request #402 from dnephin/fig_ports.rebase
Fig port command
2014-09-05 11:09:40 -07:00
Daniel Nephin
f5d43b6452 Resolves #455 - volumes_from a service with no containers.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-04 22:21:49 -04:00
Daniel Nephin
c48ee5caef Add a new fig command for retrieving the locally bound port of a service.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-09-04 22:09:12 -04:00
Ben Firshman
2827786886 Merge pull request #364 from docker/non-numeric-link-alias
Non-numeric link alias
2014-09-04 20:39:18 +01:00
Aanand Prasad
7ad91f3f00 Merge pull request #442 from dnephin/fix_create_container_volumes
Additional validation for container volumes and ports.
2014-09-04 12:12:09 -07:00
Daniel Nephin
24044fa704 Some additional validation for container ports, and a couple extra test cases.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-31 02:00:58 -04:00
Daniel Nephin
07fa169fd2 Resolves #260, #301 and #449
Adds ~ support and ro mode support for volumes, along with some additional validation.

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-31 02:00:52 -04:00
Daniel Nephin
8157f0887d Fix the return value of get_tty_width() it should return an int.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-25 22:20:07 -04:00
Aanand Prasad
6dab8c1b89 Merge pull request #420 from bfirsh/inline-installation-commands
Collapse install instructions into single line
2014-08-21 17:28:08 -07:00
Ben Firshman
63bd05d40e Collapse install instructions into single line
For maximum copy and paste happiness

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-18 13:36:37 -07:00
Luke Amdor
e51851c884 adding "fig pull [SERVICE]" to pull service images
Fixes #158

Signed-off-by: Luke Amdor <luke.amdor@gmail.com>
2014-08-15 09:24:15 -05:00
Mark Steve Samson
e224c4caa4 Add integration test for restart command
Signed-off-by: Mark Steve Samson <hello@marksteve.com>
2014-08-13 10:01:27 +08:00
Aanand Prasad
dc857a7ad5 Merge pull request #404 from bfirsh/validate-dco
Validate DCO on Travis
2014-08-12 11:20:38 -07:00
Ben Firshman
aca0e42178 Validate DCO on Travis
Copied straight from Docker, replacing github.com/docker/docker
with github.com/docker/fig in .validate.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-12 10:58:16 -07:00
Ben Firshman
15037ce0e5 Merge pull request #397 from alunduil/master
Exclude tests package from installation.
2014-08-12 18:16:50 +01:00
Mark Steve Samson
9d55e01e2a Implement restart command (Closes #98)
Signed-off-by: Mark Steve Samson <hello@marksteve.com>
2014-08-12 10:07:20 +08:00
Aanand Prasad
8eee2bf913 Merge pull request #396 from dnephin/remove_extra_calls
Remove extra calls, test bug fixes
2014-08-11 11:54:57 -07:00
Alex Brandt
3965db9dff Exclude tests package from installation.
Installing the top-level tests package is asking for conflicts with
other python packages and isn't required to run fig.  This simply lets
find_packages know to ignore tests and any sub-packages.
2014-08-11 09:00:16 -05:00
Daniel Nephin
294453433d Remove extra calls to docker server.
Fix broken integration tests

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-10 11:24:20 -04:00
Chris Corbyn
22f897ed09 Merge pull request #370 from dnephin/debug_option
Add a --debug flag for debugging docker calls
2014-08-10 21:36:17 +10:00
Daniel Nephin
df7c2cc43f Resolves #369, add verbose output on --verbose flag
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-09 21:05:54 -04:00
Aanand Prasad
f2bf7f9e0d Merge pull request #384 from timfreund/master
Enable monochrome output in the 'up' and 'logs' commands
2014-08-08 14:55:31 -07:00
Tim Freund
69c241ba12 Enable monochrome output in the 'up' and 'logs' commands
Some systems, like Jenkins or other build servers, cannot correctly
render ANSI color codes.  The '--no-color' option enables monochrome
output in the 'up' and 'logs' commands to improve readability in those
systems.

Signed-off-by: Tim Freund <tim@freunds.net>
2014-08-08 16:15:27 -04:00
Aanand Prasad
59e31ff544 Update docs to remove numeric suffix
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-08-08 13:08:27 -07:00
Aanand Prasad
62a4d214e8 Default link alias which is just the service name
Closes #37.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-08-08 13:05:42 -07:00
Aanand Prasad
73bd4aca74 Use hostnames everywhere in docs, add YAML note and deprecate env.md
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-08-08 11:58:41 -07:00
Aanand Prasad
342ed948ec Merge pull request #379 from docker/use-library-python-image-for-dockerfile
Use library python image for dockerfile
2014-08-07 17:06:38 -07:00
Ben Firshman
3fc7ad3291 Don't use deprecated orchardup/python image
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 16:43:59 -07:00
Aanand Prasad
a39460d7b2 Merge pull request #392 from bfirsh/more-official-images
More official images
2014-08-07 16:42:36 -07:00
Chris Corbyn
6ab084a338 Merge pull request #390 from bfirsh/remove-integration-tests-from-ci
Remove integration tests from ci
2014-08-08 08:37:12 +10:00
Ben Firshman
406425a6b9 Use official images in rails tutorial
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:54:59 -07:00
Chris Corbyn
b690b0d20e Merge pull request #387 from dnephin/make_ps_work_on_jekins
tty width on jenkins
2014-08-08 07:54:00 +10:00
Ben Firshman
796df302dd Use official images on getting started guide
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:50:55 -07:00
Aanand Prasad
1346805bef Merge pull request #391 from bfirsh/repository-move
github.com/docker/fig
2014-08-07 14:49:26 -07:00
Ben Firshman
16744bc78b Switch to official images in readme
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:48:29 -07:00
Ben Firshman
fc3c12ad90 Update repository URL
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:42:49 -07:00
Ben Firshman
bbcbe9df9f Upload PyPi package manually
This never worked properly anyway.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:36:49 -07:00
Ben Firshman
1a240f50ae Remove integration tests from Travis
Orchard is going away and Travis will be able to run these soon
anyway.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:35:22 -07:00
Ben Firshman
47970761c5 Remove Orchard message from readme
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 14:19:27 -07:00
Aanand Prasad
df7bc8cbb8 Merge pull request #389 from orchardup/update-irc-channel
Update IRC channel to be #docker-fig
2014-08-07 13:43:12 -07:00
Ben Firshman
ba9d744293 Update IRC channel to be #docker-fig
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 13:39:27 -07:00
Aanand Prasad
d5854bb625 Merge pull request #388 from orchardup/dockerize
Point at Docker website and Docker IRC channel
2014-08-07 10:16:16 -07:00
Ben Firshman
e5f7690137 Point at Docker website and Docker IRC channel
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-07 10:15:01 -07:00
Daniel Nephin
b0f398caaa Resolves #386, tty width on jenkins
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-08-07 12:30:01 -04:00
Aanand Prasad
0a15e7fe9c Merge pull request #378 from orchardup/use-ruby-image-for-rails-tutorial
Use ruby image for rails docs
2014-08-06 16:51:04 -07:00
Aanand Prasad
255b9419dd Merge pull request #382 from orchardup/use-official-repositories-in-django-example
Use official images in Django example
2014-08-06 16:50:45 -07:00
Aanand Prasad
c63947b5e2 Merge pull request #383 from orchardup/use-official-repos-on-homepage
Use official repos on home page
2014-08-06 16:40:48 -07:00
Ben Firshman
b5d1979d58 Use official repos on home page
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-06 16:33:31 -07:00
Ben Firshman
94aa097bc3 Use official images in Django example
Much neater.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-06 16:30:27 -07:00
Aanand Prasad
72ce7ce374 Merge pull request #380 from orchardup/use-hostname-for-redis-on-home-page-example
Use hostname on home page example
2014-08-06 16:11:44 -07:00
Ben Firshman
6d64f20ad6 Use hostname on home page example
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-06 15:53:45 -07:00
Ben Firshman
506f54e9c3 Use ruby image for rails docs
Fixes #376

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-08-06 13:42:56 -07:00
Chris Corbyn
90f5eda930 Merge pull request #367 from dnephin/fix_366_python_dependencies
Don't pin versions in setup.py install_requires
2014-08-05 08:17:23 +10:00
Daniel Nephin
c0450f7df0 Resolves #366, non-pinned versions in setup.py:install_requires
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-07-29 17:13:22 -07:00
d11wtq
847ec5b559 Update dockerpty -> 0.2.3
Signed-off-by: d11wtq <chris@w3style.co.uk>
2014-07-29 01:17:28 +00:00
Aanand Prasad
09ffa101ed Merge pull request #362 from orchardup/add-flake8-to-test-script
Add flake8 to test script
2014-07-28 16:34:07 -07:00
Ben Firshman
01e2b56405 Add flake8 to test script
For those who don't use tox.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-28 16:27:57 -07:00
Ben Firshman
2f6c763703 Merge pull request #353 from dnephin/add_flake8
Add flake8
2014-07-28 22:35:51 +01:00
Ben Firshman
4caf90c581 Merge pull request #361 from orchardup/update-release-process
Add "update the website" step to release process
2014-07-28 21:53:35 +01:00
Aanand Prasad
8fdeb46430 Add "update the website" step to release process
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-28 13:27:22 -07:00
Ben Firshman
07c47426ba Merge pull request #358 from orchardup/ship-0.5.2
Ship 0.5.2
2014-07-28 21:18:51 +01:00
Aanand Prasad
a6324d6226 Ship 0.5.2
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-28 13:17:08 -07:00
Daniel Nephin
939406ca9d Run flake8 in travis build
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-07-27 08:51:36 -07:00
Daniel Nephin
50a24bc3bf Add flake8 and fix errors.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-07-27 08:51:17 -07:00
Chris Corbyn
0dc55fda45 Merge pull request #322 from dnephin/fix_cli_for_py26
Fix cli for python 2.6
2014-07-27 18:35:40 +10:00
Chris Corbyn
dcd8e7863f Update dockerpty. Fixes #352. 2014-07-27 08:17:38 +00:00
Aanand Prasad
ed283fd3df Merge pull request #345 from orchardup/docker-inc
Docker, Inc.
2014-07-25 14:31:58 -07:00
Aanand Prasad
393433b702 Merge pull request #347 from orchardup/fix-typo-yml-docs
Fix typo yml docs
2014-07-24 14:31:19 -07:00
Ben Firshman
7516b67a14 Add link to docker-osx in yml docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-24 14:18:21 -07:00
Ben Firshman
5eac04d8d4 Fix typo in yml docs
From c09734822e

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-24 14:16:49 -07:00
Ben Firshman
fec41d3567 Merge pull request #288 from saulshanabrook/patch-1
Add warning for mapping local volumes
2014-07-24 22:15:49 +01:00
Saul Shanabrook
c09734822e Add warning for mapping local volumes
Mapping local volumes is not currently supported in boot2docker

https://github.com/boot2docker/boot2docker/issues/413
https://github.com/dotcloud/docker/issues/4023

Signed-off-by: Saul Shanabrook <s.shanabrook@gmail.com>
2014-07-24 16:37:14 -04:00
Aanand Prasad
94887a28c7 Make sure we support explicit protocols in port bindings
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-24 11:48:29 -07:00
Aanand Prasad
262efce43e Fix test regression introduced in ed80576236
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-24 11:47:39 -07:00
Ben Firshman
99064d17dd Docker, Inc.
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-24 10:24:17 -07:00
Aanand Prasad
ed80576236 Merge pull request #276 from marksteve/nocache
Add `--no-cache` option to `fig build` (Closes #152)
2014-07-23 10:13:47 -07:00
Ben Firshman
5131eaeba0 Merge pull request #335 from orchardup/document-missing-yml-options
Document missing .yml options
2014-07-23 03:45:52 +01:00
Aanand Prasad
b559880a80 Document missing .yml options
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-22 15:37:05 -07:00
Mark Steve Samson
7f06d46827 Add test for building with --no-cache
Signed-off-by: Mark Steve Samson <hello@marksteve.com>
2014-07-22 11:40:44 +08:00
Aanand Prasad
e1a0937a61 Merge pull request #330 from orchardup/www.fig.sh
www.fig.sh
2014-07-21 16:46:14 -07:00
Ben Firshman
59c976510c Update URL in readme and setup.py
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-21 16:22:03 -07:00
Ben Firshman
f189e299fd Add canonical link pointing at fig.sh
This will keep Google happy.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-21 16:20:43 -07:00
Ben Firshman
d08720247a Remove absolute link from docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-21 16:19:55 -07:00
Aanand Prasad
a4df76dd3f Merge pull request #329 from orchardup/update-to-docker-1.1.1
Update docs to Docker version 1.1.1
2014-07-21 11:46:33 -07:00
Ben Firshman
b2e3a91098 Update docs to Docker version 1.1.1
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-21 11:30:15 -07:00
Mark Steve Samson
47bbc35b74 Add --no-cache option to fig build (Closes #152)
Signed-off-by: Mark Steve Samson <hello@marksteve.com>
2014-07-20 11:28:28 +08:00
Ben Firshman
a12f3b40d5 Merge pull request #327 from orchardup/add-cname-to-github-sites
Add fig.sh CNAME to GitHub sites
2014-07-20 01:36:27 +01:00
Ben Firshman
3386927f9f Add fig.sh CNAME to GitHub sites
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-19 17:35:38 -07:00
Aanand Prasad
12d75a74e6 Merge pull request #325 from orchardup/fix-dns
Fix and document `dns:` option
2014-07-17 18:34:12 -07:00
Aanand Prasad
1a9c5e197d Merge pull request #324 from orchardup/add-maintainers-file
Add maintainers file
2014-07-17 18:32:33 -07:00
Ben Firshman
8fa85ecc05 Add maintainers file
Docker style: https://github.com/dotcloud/docker/blob/master/hack/MAINTAINERS.md

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-17 18:20:53 -07:00
Aanand Prasad
140ced6a3b Fix and document dns: option
Closes #240.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-17 18:11:50 -07:00
Daniel Nephin
779f4bda01 Fix cli for python 2.6
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-07-15 13:07:46 -07:00
Ben Firshman
0021a06468 Merge pull request #321 from orchardup/update-install-docs-for-0.5.1
Update install docs for 0.5.1
2014-07-15 00:05:13 +01:00
Aanand Prasad
6ead40e14c Update install docs for 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 16:01:41 -07:00
Aanand Prasad
5e2308d14a CHANGES.md credits for 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 15:58:26 -07:00
Ben Firshman
9ab8f358ca Merge pull request #315 from orchardup/ship-0.5.1
Ship 0.5.1
2014-07-14 23:22:48 +01:00
Aanand Prasad
d9c9b5e1f0 Ship 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 13:36:32 -07:00
Aanand Prasad
730de9187a Merge pull request #320 from orchardup/add-egg-info-to-clean
Add fig.egg-info to clean script
2014-07-14 13:35:22 -07:00
Ben Firshman
bad0f45816 Merge pull request #319 from orchardup/fix-project-name-regression
Fix regression of default behaviour in Command.project_name
2014-07-14 20:31:20 +01:00
Ben Firshman
190ea2bbd6 Add fig.egg-info to clean script
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-14 11:34:17 -07:00
Aanand Prasad
91fe414522 Fix regression of default behaviour in Command.project_name
Needed an `os.abspath` in there. Added more tests, too.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 11:32:10 -07:00
Ben Firshman
7fb43cc85f Merge pull request #283 from d11wtq/feature/optional-command
Make fig run COMMAND parameter optional.
2014-07-14 18:33:18 +01:00
Ben Firshman
d6657ed16c Merge pull request #317 from ryanbrainard/project-name-dirname
Set default project name to dir name of fig.yml
2014-07-14 18:26:43 +01:00
Ben Firshman
48e7c86d66 Merge pull request #316 from orchardup/restructure-yaml-docs
Improve fig.yml docs
2014-07-14 18:24:50 +01:00
Ryan Brainard
e9c2f2c5fb Default project_name to dirname of fig.yml
Signed-off-by: Ryan Brainard <brainard@heroku.com>
2014-07-12 23:38:20 -07:00
Aanand Prasad
197fd77b99 Improve some of the .yml options docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:55:13 -07:00
Aanand Prasad
36bef254ff Restructure .yml docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:47:43 -07:00
Aanand Prasad
8e265905d3 Add host-in-port-mapping feature to docs and changelog
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:38:18 -07:00
Chris Corbyn
ef2fb77c1d Make fig run COMMAND parameter optional.
This behaves more like the native docker client, where the absence of a
command means docker runs the CMD in the Dockerfile. If a command is
defined in fig.yml this is used instead.

Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-07-12 02:30:34 +00:00
Ben Firshman
dd5c2e8767 Merge pull request #313 from orchardup/fix-volumes-from-container
Fix volumes_from container
2014-07-12 01:10:45 +01:00
Aanand Prasad
c255999fce Merge pull request #312 from orchardup/fix-default-values-for-service-args
Remove empty lists from default Service args
2014-07-11 17:06:55 -07:00
Aanand Prasad
89341013a0 Fix volumes_from container
Closes #311.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-11 16:39:23 -07:00
Ben Firshman
f983110492 Remove empty lists from default Service args
This will cause terrifying bugs.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 16:07:31 -07:00
Aanand Prasad
9fd296f416 Merge pull request #310 from orchardup/fix-race-condition-in-recreate-containers
Fix race condition in recreate containers
2014-07-11 15:14:17 -07:00
Ben Firshman
bb89f85984 Remove stop timeout in recreate containers
This doesn't seem like a good idea – 1 second isn't enough time
to stop gracefully.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:53:56 -07:00
Ben Firshman
b573b87a92 Fix race condition in recreate containers
Container might have stopped between checking `is_running` and
calling `stop()`, which then threw an exception.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:53:46 -07:00
Aanand Prasad
036adb2de9 Merge pull request #309 from orchardup/update-docs-to-be-0.5.0
Update docs to be 0.5.0
2014-07-11 14:29:38 -07:00
Ben Firshman
36f4e30dba Add 0.5.0 to docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:16:39 -07:00
Ben Firshman
9f0cfbdfd2 Document release process
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:16:29 -07:00
Aanand Prasad
e117a7822d Merge pull request #307 from orchardup/ship-0.5.0
Ship 0.5.0
2014-07-11 14:07:40 -07:00
Ben Firshman
5489465905 Ship 0.5.0
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 13:21:01 -07:00
Ben Firshman
4afcdbdb3c Add docs for volumes_from
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 12:52:22 -07:00
Ben Firshman
94d82d4acb Merge pull request #308 from orchardup/fix-fig-up-attach
Regardless of dependencies, `fig up` only attaches to what you specify
2014-07-11 20:51:54 +01:00
Aanand Prasad
d528f9f642 Regardless of dependencies, fig up only attaches to what you specify
Without this, if you go:

    $ fig up -d db
    $ fig up web

you'll get output for both db and web, and Ctrl-C will kill them both.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-11 11:58:59 -07:00
Ben Firshman
99d7a474af Merge pull request #297 from ryanbrainard/resolve-env
Resolve environment without values to values on host
2014-07-11 18:55:40 +01:00
Ryan Brainard
d1052ff666 Add documention for key-only environment
Signed-off-by: Ryan Brainard <brainard@heroku.com>
2014-07-11 10:18:17 -07:00
Ryan Brainard
44a91e6ba8 Resolve environment without values to values on host
For parity with the Docker CLI, allow environment variables without
values to be automatically resolved to their values on the host.

Signed-off-by: Ryan Brainard <brainard@heroku.com>

Conflicts:
	tests/integration/service_test.py
2014-07-11 10:18:05 -07:00
Ben Firshman
3996947024 Merge pull request #304 from Elemecca/t-domainname
add support for domainname, qualified hostname
2014-07-11 17:40:19 +01:00
Ben Firshman
b7afaba56a Merge pull request #305 from ryanbrainard/fix-dir-typo
Fix typo in script dir name
2014-07-11 17:37:13 +01:00
Ryan Brainard
2ce3685e32 Fix typo in script dir name
Signed-off-by: Ryan Brainard <brainard@heroku.com>
2014-07-11 09:13:00 -07:00
Sam Hanes
699bbe9ca2 Split the domainname out of qualified hostnames.
Docker doesn't like it when a fully qualified hostname is passed in
the `hostname` parameter. When an FQDN is provided with `-h` the
official CLI puts the first component in `hostname` and the rest in
`domainname`. This change replicates that behavior when the user
specifies an FQDN in `hostname` in their `fig.yml`.

Signed-off-by: Sam Hanes <sam@maltera.com>
2014-07-10 20:18:09 -07:00
Ben Firshman
4b890bffde Merge pull request #290 from d11wtq/bugfix/multiplexed_non_tty
BUGFIX: Update dockerpty for non-TTY multiplexing.
2014-07-11 01:44:31 +01:00
Sam Hanes
789e1ba82b Add domainname to allowed container config.
Signed-off-by: Sam Hanes <sam@maltera.com>
2014-07-10 15:56:26 -07:00
Ben Firshman
1a9614c35e Merge pull request #293 from binarin/master
Change working dir through fig.yml
2014-07-10 10:31:57 -07:00
Alexey Lebedeff
d83bdd5164 Change working dir through fig.yml
Closes #144

Signed-off-by: Alexey Lebedeff <binarin@gmail.com>
2014-07-10 21:22:45 +04:00
Ben Firshman
e1a3fc2536 Merge pull request #303 from mozz100/master
Use yaml.safe_load instead of yaml.load
2014-07-10 09:50:51 -07:00
Richard Morrison
251aa7efb6 Use yaml.safe_load instead of yaml.load
http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML

Signed-off-by: Richard Morrison <richard@rmorrison.net>
2014-07-10 15:15:55 +01:00
Aanand Prasad
2924b9997a Merge pull request #302 from orchardup/better-fig-up-output-in-docs
Use concise fig up output in docs
2014-07-09 17:35:41 -07:00
Ben Firshman
2a9aef1332 Use concise fig up output in docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-09 17:34:39 -07:00
Chris Corbyn
361294d20b Update dockerpty for non-TTY multiplexing.
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-07-05 08:23:21 +00:00
Ben Firshman
9a825c5c35 Merge pull request #279 from orchardup/fix-run-with-no-links
Stop `fig run` starting everything when a service has no links
2014-07-02 15:46:22 +01:00
Aanand Prasad
944e15fa65 Stop fig run starting everything when a service has no links
This was thanks to the semantics of project.up(), which starts everything
if you pass it an empty list of service names. (That logic should
probably be moved out to main.py.)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-02 11:51:12 +01:00
Aanand Prasad
d04b1724ec Merge pull request #236 from rail44/feature-support-volumes-from
Support volumes_from option
2014-07-02 10:48:11 +01:00
Satoshi Amemiya
e5916b2fae Support volumes_from option
Signed-off-by: Satoshi Amemiya <rail.sky@gmail.com>
2014-07-02 13:27:54 +09:00
Mark Steve Samson
4f7cbc3812 Support for host address in port bindings (Closes #267) 2014-06-27 15:07:22 +01:00
Aanand Prasad
3c48884dbb Update dockerpty; stub it out in tests
Its current behaviour occasionally causes tests to hang; until this is
resolved, we'll stub it out. We weren't testing the output of 'run'
anyhow (though we should be).
2014-06-27 14:41:36 +01:00
Aanand Prasad
7ec63afae9 Merge pull request #272 from d11wtq/bugfix/race_condition
Update dockerpty to fix discovered race condition.
2014-06-26 13:07:15 +01:00
Chris Corbyn
8c6b516aa0 Update dockerpty to fix discovered race condition.
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-26 11:41:43 +00:00
Aanand Prasad
50c588176c Merge pull request #270 from d11wtq/bugfix/tty_size
Use dockerpty instead for pseudo-tty behaviour (fixes TTY size issue #253 & #97)
2014-06-25 17:00:56 +01:00
Chris Corbyn
3770aac1af Use dockerpty instead for pseudo-tty behaviour.
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-25 15:54:52 +00:00
Aanand Prasad
256dccc554 Merge pull request #268 from orchardup/update-docker-py-0.3.2
Update to docker-py 0.3.2
2014-06-25 14:16:48 +01:00
Ben Firshman
d0f65906ed Update to docker-py 0.3.2
From 59ced5700c

Now only supports Docker 1.0.0 / remote API v1.12.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-06-25 11:56:20 +01:00
Ben Firshman
95aa61cfe5 Merge pull request #246 from d11wtq/feature/auto_start
Feature: `fig up` and `fig run` now start linked containers (closes #31).
2014-06-24 14:46:47 +01:00
Chris Corbyn
247691ca44 Remove auto_start option from fig.yml.
This option is redundant now that services can be started along with links.

Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-21 10:39:36 +00:00
Chris Corbyn
0fc9cc65d1 Rename '--only' => '--no-deps'
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-21 10:30:36 +00:00
Ben Firshman
eb69225444 Merge pull request #209 from orchardup/better-invalid-service-name-error
Better error message when service names are invalid
2014-06-20 12:32:07 +01:00
Aanand Prasad
cafe68a92d Better error message when service names are invalid 2014-06-20 12:10:18 +01:00
Ben Firshman
723cccdae8 Merge pull request #258 from j0hnsmith/support_net_param
add net param support
2014-06-19 12:19:06 +01:00
j0hnsmith
6b8044e92c add net param support
Signed-off-by: Peter Flood <info@whywouldwe.com>
2014-06-19 11:57:55 +01:00
Aanand Prasad
1e7e8202af Merge pull request #257 from orchardup/use-ubuntu-image-to-build-docs
Use ubuntu:13.10 image to build docs
2014-06-18 17:31:23 +01:00
Ben Firshman
c0fdf7bd39 Use ubuntu:13.10 image to build docs
stackbrew/ubuntu and ubuntu are the same thing now.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-06-18 17:26:38 +01:00
Aanand Prasad
034b66fedb Merge pull request #256 from orchardup/ship-0.4.2
Ship 0.4.2
2014-06-18 16:45:21 +01:00
Ben Firshman
eed274c632 Ship 0.4.2 2014-06-18 16:32:23 +01:00
Ben Firshman
5b10c4811f Merge pull request #255 from orchardup/fix-unicode
Fix encoding errors
2014-06-18 16:00:43 +01:00
Tobias Bradtke
2bd6e3d0a5 Do not encode chunk, just write as is. 2014-06-18 15:31:45 +01:00
Aanand Prasad
d0b5bcf26a Pass byte strings straight through LogPrinter 2014-06-18 14:51:13 +01:00
Aanand Prasad
262248d8a6 Firm up tests for split_buffer 2014-06-18 14:51:13 +01:00
Aanand Prasad
9eb3697b40 Encode all progress stream output as UTF-8
Closes #231.
2014-06-18 14:51:13 +01:00
Aanand Prasad
c246897af1 Pass script/test arguments through to nosetests 2014-06-18 14:51:13 +01:00
Aanand Prasad
cfcabce593 Extract stream_output to module 2014-06-18 14:51:12 +01:00
Aanand Prasad
e517061010 Add /venv to .gitignore 2014-06-18 14:51:12 +01:00
Aanand Prasad
feb8ad7b4c Update Dockerfile reference/tutorial links 2014-06-16 23:32:50 +01:00
Chris Corbyn
1b5bf6e12a Merge remote-tracking branch 'upstream/master' into feature/auto_start
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>

Conflicts:
	tests/fixtures/simple-figfile/fig.yml
	tests/unit/project_test.py
2014-06-12 22:05:51 +00:00
Aanand Prasad
e953a32a82 Merge pull request #248 from orchardup/docker-1.0.0
Update to docs to Docker 1.0.0
2014-06-11 12:13:09 -07:00
Ben Firshman
f1390b3cb6 Merge pull request #249 from d11wtq/performance/busybox_fixtures
Use busybox in fixtures, instead of ubuntu
2014-06-11 19:59:21 +01:00
d11wtq
6e485df084 Rename --no-links to --only
Signed-off-by: d11wtq <chris@w3style.co.uk>
2014-06-11 10:50:45 +00:00
d11wtq
3a342fb25d Use busybox in fixtures, instead of ubuntu
Signed-off-by: d11wtq <chris@w3style.co.uk>
2014-06-11 10:25:50 +00:00
Chris Corbyn
e71e82f8ac Add missing fixture file
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-11 10:09:54 +00:00
Ben Firshman
da80eca28c Update to docs to Docker 1.0.0
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-06-10 14:58:06 -07:00
d11wtq
1d1e23611b Rename --keep-old to --no-recreate
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:51 +00:00
d11wtq
74e067c6e6 Document --keep-old flag in CLI reference
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:50 +00:00
d11wtq
85b9619799 Document 'auto_start' in fig.yml
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:50 +00:00
d11wtq
ab1fbc96c3 Move keep_old check up into Project
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:50 +00:00
d11wtq
a04143e2a7 Remove unused: from __future__ import unicode_literals.
This is not being used and it confuses the Command class. Rather
than try to fix the Command class, I've taken the pragmatic
approach and removed the trigger that confuses it.

Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:50 +00:00
d11wtq
6c4299039a Write integration tests for --keep-old in the CLI
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:49 +00:00
d11wtq
655d347ea2 Write integration tests on new fig run linking behaviour
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:49 +00:00
d11wtq
94a3164248 Re-add missing import for patch
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:49 +00:00
d11wtq
18728a64b9 Write tests for --no-links changes to fig up
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:48 +00:00
d11wtq
d8b0fa294e Add integration tests for Project.up() w/ start_links and keep_old
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:48 +00:00
d11wtq
a6c8319b5d Add integration tests for Service.recreate_containers() with keep_old
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:48 +00:00
d11wtq
5d92f12f8e Add unit tests for include_links in get_services()
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:48 +00:00
d11wtq
c0231bdb70 Rename _prepend_with_links() -> _inject_links()
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:47 +00:00
d11wtq
ac541e208f Remove obsolete method _get_linked_service_names()
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:47 +00:00
d11wtq
3d8ce448b8 Spike: Re-use existing containers for fig run
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:47 +00:00
d11wtq
949df97726 Fix issue with infinite recursion when service_names is empty
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:46 +00:00
d11wtq
14cbe40543 Update doc string to reflect new behaviour.
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:46 +00:00
d11wtq
9dd53ecdaa Fix bug with duplicate service entries in fig up
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:46 +00:00
d11wtq
6bfe5e049d Spike: Implement --no-links for fig up
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:46 +00:00
d11wtq
b672861ffd Spike: Start linked containers on fig run by default
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:45 +00:00
d11wtq
b081077f2b Remove FIXME, as there's nothing to fix :)
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:45 +00:00
d11wtq
13a296049b Update cli integration test for 'auto_start' behaviour
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:45 +00:00
d11wtq
22c531dea7 Add unit tests for Project.get_services()
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:44 +00:00
d11wtq
dfc74e2a77 Write integration test for 'auto_start' behaviour
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:44 +00:00
d11wtq
0c12db06ec Move 'auto_start' option default to Service and add unit tests
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:44 +00:00
d11wtq
edf6b56016 Spike: Add --up option to fig run
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:43 +00:00
d11wtq
8b4ed0c1a8 Spike: Add 'auto_start' option to fig.yml
Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-06-09 01:31:27 +00:00
Ben Firshman
1b5335f409 Add developer certificate of origin docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-05-28 11:08:46 +01:00
Ben Firshman
3a2c9c1016 Switch to Apache License 2.0
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-05-28 10:59:21 +01:00
Aanand Prasad
cf3eed2cda Merge pull request #227 from orchardup/add-pythonunbuffered-to-django-docs
Add PYTHONUNBUFFERED=1 to Django tutorial
2014-05-22 17:12:23 +01:00
Ben Firshman
2ecd366905 Add PYTHONUNBUFFERED=1 to Django tutorial 2014-05-22 14:15:17 +01:00
Ben Firshman
d34dc45b78 Merge pull request #223 from orchardup/put-orchard-in-sidebar
Put Orchard in docs sidebar
2014-05-14 16:35:37 +01:00
Aanand Prasad
8394e84099 Compress the sidebar a bit 2014-05-14 16:32:39 +01:00
Ben Firshman
adda3a7f79 Put Orchard in docs sidebar 2014-05-14 13:05:40 +01:00
Aanand Prasad
52d0f4d9e7 Merge pull request #206 from orchardup/remove-intermediate-build-containers
Remove intermediate build containers
2014-05-12 11:59:01 +01:00
Ben Firshman
c1a38d787d Fix 0.4.1 release notes 2014-05-12 11:45:55 +01:00
Ben Firshman
7879dfd3fd Fix deploy docs script 2014-05-09 10:59:44 +01:00
Ben Firshman
cd1c8b2f09 Update docs to Docker 0.11.1 2014-05-09 10:53:12 +01:00
Ben Firshman
7a9228ad75 Remove intermediate build containers
Docker does this by default now.
2014-05-08 15:40:53 +01:00
Aanand Prasad
98ceb62202 Update Fig version on install page 2014-05-08 13:10:13 +01:00
Ben Firshman
b99bb64487 Add sanity check to OS X build script 2014-05-08 13:03:21 +01:00
Ben Firshman
580affa5f3 Add sanity check to linux build script 2014-05-08 13:02:50 +01:00
Ben Firshman
d600b3498b Remove entrypoint from dockerfile 2014-05-08 13:00:39 +01:00
Aanand Prasad
f0eaf84cb9 Merge pull request #217 from orchardup/0.4.1
Ship 0.4.1
2014-05-08 12:51:35 +01:00
Ben Firshman
257a171c0c Ship 0.4.1 2014-05-08 12:43:09 +01:00
Ben Firshman
3c5e818b49 Update install docs for Docker 0.11.0 2014-05-08 12:21:46 +01:00
Ben Firshman
c3c8395cef Merge pull request #215 from marksteve/docker-0.11
Docker 0.11
2014-05-08 10:10:19 +01:00
Mark Steve Samson
38c008e527 Fix index error when getting ghost state of containers 2014-05-08 12:39:15 +08:00
Aanand Prasad
a3d8f7d113 Remove hash from gif URL 2014-05-06 17:13:56 +01:00
Ben Firshman
65a642097c Merge pull request #211 from servebox/project-name
Add the ability to configure the project name using a command line option
2014-05-05 10:50:48 +01:00
Ben Firshman
dff9aa6f0c Add installation and entrypoint to dockerfile 2014-05-05 10:50:23 +01:00
Ben Firshman
ab145b5365 Fix pull requests failing on travis 2014-05-05 10:50:10 +01:00
Jef Mathiot
5878fe3834 Add the ability to configure the project name 2014-05-02 18:00:58 +02:00
Aanand Prasad
715e29d7ba Merge pull request #207 from orchardup/fig-run-correct-exit-code
Return correct exit code from fig run
2014-05-01 18:21:40 +01:00
Ben Firshman
983337401c Return correct exit code from fig run
Closes #197
2014-05-01 18:17:12 +01:00
Ben Firshman
8251dec587 Add docs, build and dist to clean script 2014-05-01 17:17:23 +01:00
Ben Firshman
52f994cf04 Remove /docs/.git-gh-pages from gitignore
It's inside /docs/_site
2014-05-01 17:17:06 +01:00
Ben Firshman
3d4b5cfbfe Update version of docker-osx 2014-05-01 15:34:21 +01:00
Aanand Prasad
33b057bfaf Update tag in docker-osx download URL 2014-05-01 10:51:42 +01:00
Aanand Prasad
629fe771df Update domain in docker-osx download URL 2014-04-30 16:37:20 +01:00
Aanand Prasad
5e6f175b5f Update download URLs on install page 2014-04-30 16:26:46 +01:00
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
Ben Firshman
deb7f3c5b6 Bump to version 0.1.2 2014-01-22 13:37:37 +00:00
Ben Firshman
48eb5e5c82 Merge pull request #40 from orchardup/fix-35
Make sure attach() is called as soon as LogPrinter is initialized
2014-01-22 05:14:05 -08:00
Aanand Prasad
65071aafb0 Make sure attach() is called as soon as LogPrinter is initialized
Fixes #35.
2014-01-22 13:12:51 +00:00
Ben Firshman
4ee87a7029 Merge pull request #39 from orchardup/fix-cursor-lag
Fix lag when using cursor keys in an interactive 'fig run'
2014-01-20 16:12:29 -08:00
Aanand Prasad
977ec7c941 Remove unused import 2014-01-20 19:25:28 +00:00
Aanand Prasad
40d04a076c Fix lag when using cursor keys in an interactive 'fig run' 2014-01-20 19:23:50 +00:00
Ben Firshman
4646ac85b0 Add PyPi badge 2014-01-20 18:41:04 +00:00
Ben Firshman
8773bad99a Merge pull request #34 from orchardup/better-tty-handling-for-fig-run
Add option to disable pseudo-tty on fig run
2014-01-20 10:19:13 -08:00
Aanand Prasad
084db337a0 Update docker-py
Brought in changes from https://github.com/dotcloud/docker-py/pull/145
2014-01-20 18:09:25 +00:00
Aanand Prasad
f47f075f02 Merge pull request #36 from orchardup/use-container-create-for-recreating-containers
Use Container.create to recreate containers
2014-01-20 09:59:02 -08:00
Ben Firshman
7abc4fbf3a Improve ps CLI test 2014-01-20 16:50:41 +00:00
Ben Firshman
855a9c623c Remove containers after running CLI tests 2014-01-20 16:47:58 +00:00
Ben Firshman
7e2d86c510 Use Container.create to recreate containers
self.create_container might do unexpected things.
2014-01-20 16:10:54 +00:00
Aanand Prasad
405079f744 Use raw socket in 'fig run', simplify _attach_to_container 2014-01-20 15:52:07 +00:00
Ben Firshman
fc1bbb45b1 Add option to disable pseudo-tty on fig run
Also disable tty if stdin is not a tty.
2014-01-19 20:33:06 +00:00
Ben Firshman
24a6d1d836 Forcibly kill Docker when Travis ends
May fix tests timing out.
2014-01-19 20:11:53 +00:00
Ben Firshman
f3d273864d Add comments to script/travis 2014-01-19 20:11:53 +00:00
Ben Firshman
ce8ef7afe7 Merge pull request #33 from cameronmaske/recreate_containers
Patch for #32
2014-01-19 12:11:05 -08:00
Cameron Maske
62bba1684b Updated recreate_containers to attempt to base intermediate container's the previous container's image.
Added in additional functionality to reset any entrypoints for the intermediate container and pull/retry handling if the image does not exist.
Updated test coverage to check if an container is recreated with an entrypoint it is handled correctly.
2014-01-19 18:40:21 +00:00
Ben Firshman
07f3c78369 Update docker-py
From 4bc5d27e51
2014-01-19 16:50:08 +00:00
Ben Firshman
cbfbb9899d Merge pull request #30 from orchardup/ship-0.1.1
Bump to version 0.1.1
2014-01-17 11:38:49 -08:00
Ben Firshman
b428988ef6 Bump to version 0.1.1 2014-01-17 19:14:48 +00:00
Ben Firshman
4c72598e68 Merge pull request #29 from orchardup/fix-external-port-config
Fix external port config
2014-01-17 10:43:54 -08:00
Aanand Prasad
c6e19e34f7 Fix external port config
When exposing a port externally, it seems Docker only actually exposes it
if you specify the *internal* port as `xxxx/tcp`. So add that on if it's
not there.
2014-01-17 18:01:27 +00:00
Aanand Prasad
c0dbb1c2ec Merge pull request #24 from orchardup/travis-pypi-deployment
Travis PyPi deployment
2014-01-16 11:12:40 -08:00
Ben Firshman
4bec39535f Remove unnecessary release script 2014-01-16 18:52:15 +00:00
Ben Firshman
7c1ec74cf6 Add Travis PyPi deployment 2014-01-16 18:51:50 +00:00
Ben Firshman
8b77b51c15 Fix travis formatting
To use formatting travis command line tool uses
2014-01-16 18:49:15 +00:00
Aanand Prasad
7070e06ac6 Thank some folks 2014-01-16 18:45:38 +00:00
Aanand Prasad
5b9c228cf8 Add note to CHANGES.md about old/new env vars 2014-01-16 18:42:03 +00:00
Ben Firshman
15e8c9ffbb Fix ambiguous order of upgrade instructions 2014-01-16 18:35:41 +00:00
Aanand Prasad
caccf96d3f Fix markdown 2014-01-16 18:32:35 +00:00
Ben Firshman
720cc192bb Add upgrade instructions 2014-01-16 18:27:06 +00:00
Ben Firshman
573fae089f Add missing files to manifest 2014-01-16 18:21:56 +00:00
Ben Firshman
83cd16e373 Merge pull request #22 from orchardup/ship-0.1.0
Ship 0.1.0
2014-01-16 10:19:44 -08:00
Aanand Prasad
74fb400fef Version 0.1.0 2014-01-16 18:18:01 +00:00
Ben Firshman
a07b00606b Fix typos in manifest 2014-01-16 18:17:16 +00:00
Ben Firshman
65f23583ae Ignore vim undo files 2014-01-16 18:16:26 +00:00
Aanand Prasad
c8a58f2547 Merge pull request #23 from orchardup/add-scale-command
Add scale command
2014-01-16 10:14:39 -08:00
Ben Firshman
56c6efdfce Add scale command
Closes #9
2014-01-16 18:09:46 +00:00
Ben Firshman
8ed86ed551 Add number to container 2014-01-16 18:05:59 +00:00
Aanand Prasad
d2f4c81d62 Merge pull request #21 from orchardup/add-basic-dockerfile
Add basic Dockerfile
2014-01-16 09:44:44 -08:00
Ben Firshman
b20190da98 Add basic Dockerfile
Because.
2014-01-16 17:28:47 +00:00
Ben Firshman
5d9a5a0c84 Merge pull request #20 from orchardup/travis-just-uses-socket
Travis runs Docker just on the Unix socket, not on TCP
2014-01-16 09:15:40 -08:00
Aanand Prasad
bb7613f37b Travis runs Docker just on the Unix socket, not on TCP 2014-01-16 17:03:56 +00:00
Aanand Prasad
9bd54d7be2 Merge pull request #19 from orchardup/use-docker-host-variable
Use DOCKER_HOST environment variable to find Docker daemon
2014-01-16 09:03:37 -08:00
Aanand Prasad
5c8fac5993 Merge pull request #18 from orchardup/shorten-long-commands-in-ps
Shorten long commands in ps
2014-01-16 08:47:25 -08:00
Aanand Prasad
3e2fd6a2a1 Use DOCKER_HOST environment variable to find Docker daemon
Removed all "smart" connection logic. Fig either uses the DOCKER_HOST
environment variable if it's present, or passes `None` to docker-py,
which does the "right thing" (i.e. falls back to the Unix socket).

This means we no longer know at URL-deciding time whether we can connect
to the Docker daemon, so we wrap `dispatch` in a `try/except` which
catches `requests.exceptions.ConnectionError`.
2014-01-16 16:40:55 +00:00
Ben Firshman
f36dd414a0 Merge pull request #10 from orchardup/use-regular-attach
LogPrinter uses regular attach(), not websocket
2014-01-16 08:27:48 -08:00
Aanand Prasad
af1b0ed088 Account for length of the ellipsis string when truncating commands 2014-01-16 14:07:17 +00:00
Aanand Prasad
feafea2c6d LogPrinter uses regular attach(), not websocket
Fixes #7.
2014-01-16 14:04:10 +00:00
Ben Firshman
c4f5ed839f Shorten long commands in ps 2014-01-16 14:02:52 +00:00
Ben Firshman
b4c905dc83 Merge pull request #14 from orchardup/vendor-docker-py
Vendor docker-py
2014-01-16 05:49:15 -08:00
Aanand Prasad
804e2cdcb1 Merge pull request #16 from orchardup/add-help-command
Add help command
2014-01-16 05:40:48 -08:00
Aanand Prasad
0bb5e48f53 Merge pull request #15 from orchardup/copy-readme-command-docs-to-docstrings
Copy readme commands docs to CLI docstrings
2014-01-16 05:40:43 -08:00
Aanand Prasad
21528f08d4 Vendor docker-py
From 9dc03c5737
2014-01-16 13:30:01 +00:00
Ben Firshman
b1e7f548f4 Add help command 2014-01-16 13:26:25 +00:00
Ben Firshman
cdcea98290 Copy readme commands docs to CLI docstrings 2014-01-16 13:17:00 +00:00
Ben Firshman
c06456da37 Merge pull request #13 from orchardup/recreate-containers
Recreate containers on each `fig up`
2014-01-16 04:57:48 -08:00
Aanand Prasad
7b31fdf6f6 Clarify that volumes are preserved when recreating containers 2014-01-16 12:42:07 +00:00
Aanand Prasad
e38b403b14 Update README for new 'fig up' behaviour 2014-01-16 12:32:16 +00:00
Aanand Prasad
ee0c4bf690 Fix test regression 2014-01-16 12:32:16 +00:00
Aanand Prasad
8c583d1bb2 Quieter log output when recreating
Moved log stuff to Service, which I think makes more sense anyway.
Maybe.
2014-01-16 12:32:16 +00:00
Aanand Prasad
ea4753c49a Use an anonymous intermediate container so that when recreating containers, suffixes always start from 1 2014-01-16 12:32:15 +00:00
Aanand Prasad
3956d85a8c Refactor recreate_containers() in preparation for smart name-preserving logic 2014-01-16 12:32:15 +00:00
Aanand Prasad
8a0071d9c1 Reduce stop() timeout when recreating containers 2014-01-16 12:32:15 +00:00
Aanand Prasad
5db6c9f51b Rework 'fig up' to use recreate_containers() 2014-01-16 12:32:15 +00:00
Aanand Prasad
f5f9357736 Remove project.create_containers(), revamp project.recreate_containers()
`recreate_containers` now returns two lists of old+new containers, along
with their services.
2014-01-16 12:32:15 +00:00
Aanand Prasad
bdc6b47e1f service.recreate_containers() no longer removes the old containers
We need to keep them around until the new ones have been started.
2014-01-16 12:32:15 +00:00
Aanand Prasad
3669236aa1 Support volumes in config with an unspecified host path 2014-01-16 12:32:15 +00:00
Aanand Prasad
207e83ac2f Be sure to test that recreate_containers updates config 2014-01-16 12:32:15 +00:00
Ben Firshman
3c5e334d9d Add recreate_containers method to service 2014-01-16 12:32:15 +00:00
Aanand Prasad
a8e275a432 Implement UserError __unicode__ method 2014-01-16 12:32:04 +00:00
Aanand Prasad
887a30e327 Clarify when 'fig stop' is necessary in README 2014-01-16 12:07:18 +00:00
Ben Firshman
7a1fb3a8d2 Fix ordering of port mapping 2014-01-16 01:54:05 +00:00
Ben Firshman
d4000e07a9 Switch order of connection logic so TCP is tried first 2014-01-16 00:58:46 +00:00
Aanand Prasad
a3d024e11d Larger gif in README 2014-01-14 19:19:15 +00:00
Aanand Prasad
b92e998929 'fig logs' shows output for stopped containers 2014-01-14 12:42:30 +00:00
Ben Firshman
f448a841c5 New docker-osx installation instructions 2014-01-12 16:58:50 +00:00
Ben Firshman
c9c844c279 Print commands travis scripts are running 2014-01-11 14:53:07 +00:00
Ben Firshman
342f187318 Put python egg cache in a writeable dir 2014-01-11 14:52:37 +00:00
Ben Firshman
d063f0e00c Add back missing compat module 2014-01-11 14:31:56 +00:00
Ben Firshman
0614e2c590 Use Docker 0.7.5 on Travis 2014-01-11 14:31:47 +00:00
Ben Firshman
431b3dc2b2 Move Travis badge out of heading 2014-01-11 14:17:00 +00:00
Ben Firshman
544cd884ee Use Docker 0.7.4 on Travis
Also use a package that doesn't disappear and break the tests.
2014-01-11 14:14:35 +00:00
Ben Firshman
c6efb45585 Exit travis-install script on error 2014-01-11 14:14:35 +00:00
Aanand Prasad
38008a87e8 Gif. 2014-01-10 20:42:00 +00:00
Aanand Prasad
059d240824 Fix line buffering when there's UTF-8 in a container's output 2014-01-09 16:19:22 +00:00
Aanand Prasad
7a4b69edc0 Remove compat texttable module - breaks on Python 2.7 2014-01-09 15:32:59 +00:00
Aanand Prasad
8cab05feb4 Failing (on 2.7, at least) smoke test for 'fig ps'
See #8.
2014-01-09 15:32:50 +00:00
Aanand Prasad
892677a9d3 Very basic CLI smoke test
See #8.
2014-01-09 15:32:24 +00:00
Ben Firshman
00a1835fae Allow Python 3 to fail
docker-py is broken
2014-01-06 17:58:50 +00:00
Ben Firshman
7888027425 Put requirements back in .txt files
Read-only FS in travis
2014-01-06 17:58:50 +00:00
Ben Firshman
0760ea1b00 Add Python 3 and PyPy to .travis.yml 2014-01-06 17:58:50 +00:00
Christopher Grebs
31f0907732 Add unicode_literals to main module 2014-01-06 17:58:50 +00:00
Christopher Grebs
9bec059cc7 e.explanation a 'str' object 2014-01-06 17:58:50 +00:00
Christopher Grebs
f600fa8bf3 More future imports for safety 2014-01-06 17:58:50 +00:00
Christopher Grebs
c6e91db32f Add texttable compat module that is py3k compatible 2014-01-06 17:58:50 +00:00
Christopher Grebs
b101118d1e Add future import for print function 2014-01-06 17:58:50 +00:00
Christopher Grebs
30ea4508c3 Use print function 2014-01-06 17:58:50 +00:00
Christopher Grebs
3c91315426 Fix exception alias syntax 2014-01-06 17:58:49 +00:00
Christopher Grebs
bf8875d930 Added tox file 2014-01-06 17:58:49 +00:00
Christopher Grebs
93b9b6fd9f First version with python3 support.
* Moved requirements*.txt files to proper spec definitions in setup.py
 * Added a new fig.compat module to store some compatibility code
2014-01-06 17:58:49 +00:00
Ben Firshman
f96a1a0b35 Fix Python 2.6 2014-01-06 17:58:49 +00:00
Ben Firshman
8de07ccf65 Add Travis badge 2014-01-06 11:34:44 +00:00
Ben Firshman
ff9fa5661d Fix Python 2.6 2014-01-06 11:22:46 +00:00
Ben Firshman
17b9cc430c Add Travis CI 2014-01-06 11:13:36 +00:00
Ben Firshman
d8a2a0f003 Merge pull request #4 from tomstuart/be-friendlier-about-missing-fig-yml
Be friendlier about missing `fig.yml`
2014-01-03 04:11:26 -08:00
Tom Stuart
490742b892 Emit a friendly error when fig.yml is missing
I keep doing this by accident, so I'd rather not see the stack trace.
2014-01-03 11:59:03 +00:00
Ben Firshman
3fa80cd974 Add note about fig rm/build dance
This needs more thought. Ref #2
2014-01-02 23:47:31 +00:00
Ben Firshman
9ede185d4b Merge pull request #3 from tomstuart/fix-fig-build-logging
Fix `fig build` logging
2014-01-02 15:37:10 -08:00
Tom Stuart
aaf90639a0 Include service name in log message 2014-01-02 23:28:21 +00:00
Tom Stuart
5ba7040df2 Make logger available in project.py 2014-01-02 23:27:47 +00:00
Ben Firshman
3d411ed0bb Remove monospace font from command headings 2014-01-02 23:05:58 +00:00
Ben Firshman
dd1f8934ad Fix markdown formatting 2014-01-02 22:48:51 +00:00
Ben Firshman
febcbcddb9 Revert "Update tag line"
This reverts commit 88c74d67f6.
2014-01-02 22:36:04 +00:00
Ben Firshman
88c74d67f6 Update tag line 2014-01-02 22:31:14 +00:00
Ben Firshman
0fb915e57e Add commands section to readme 2014-01-02 22:20:54 +00:00
Ben Firshman
e0a21e3df4 Bump version to 0.0.2 2014-01-02 21:30:28 +00:00
Ben Firshman
45b7bd4361 Readme tweaks 2014-01-02 21:23:47 +00:00
Ben Firshman
36002f95ed Try connecting to localdocker:4243
See https://github.com/noplay/docker-osx/pull/22
2014-01-02 20:51:35 +00:00
Ben Firshman
0a92cbfa4d Fix readme formatting 2014-01-02 20:19:00 +00:00
Ben Firshman
5b1fd64708 Add getting started guide to readme 2014-01-02 20:08:01 +00:00
Ben Firshman
9b289b6f3b Stop "fig up" containers gracefully
With double ctrl-c force.
2014-01-02 19:18:08 +00:00
Ben Firshman
6d0702e607 Send log output to stderr 2014-01-02 18:31:00 +00:00
Aanand Prasad
38478ea504 Full fig.yml and environment variable reference 2014-01-02 17:01:27 +00:00
Ben Firshman
a39db86651 Add "fig build" command 2014-01-02 15:28:33 +00:00
Ben Firshman
853d8ad280 Namespace tests inside a project
So it doesn't delete all your containers for every test. Cool.
2014-01-02 15:27:51 +00:00
Ben Firshman
17c6ae067a Rewrite readme intro 2014-01-02 15:03:30 +00:00
Ben Firshman
770e78fdce Make usage alphabetical 2014-01-02 15:00:16 +00:00
Aanand Prasad
e5065bed16 Expand the intro a bit 2013-12-31 16:31:40 +00:00
Aanand Prasad
c0676e3fa3 Add confirmation prompt to 'fig rm' 2013-12-31 13:42:58 +00:00
Aanand Prasad
d4f3ed1840 Fix 'fig up' behaviour
- For each service, creates a container if there are none (stopped OR
  started)
- Attaches to all containers (unless -d is passed)
- Starts all containers
- On ^C, kills all containers (unless -d is passed)
2013-12-31 13:02:08 +00:00
Aanand Prasad
9ed6538693 Extract docker URL logic, use it in tests as well 2013-12-31 12:37:17 +00:00
Aanand Prasad
ff65a3e1b0 Check default socket and localhost:4243 for Docker daemon 2013-12-31 12:18:27 +00:00
Aanand Prasad
ebf9bf387c Remove unused import 2013-12-31 11:51:52 +00:00
Aanand Prasad
fdc1e0f2e1 Missing article in README 2013-12-30 18:57:55 +00:00
Aanand Prasad
f3eff9a389 Add _site to .gitignore (it's generated by Jekyll in the gh-pages branch) 2013-12-30 18:57:27 +00:00
Ben Firshman
2857631e90 Add change log 2013-12-20 21:37:55 +00:00
Ben Firshman
89cd7d8db0 Remove long description 2013-12-20 21:36:06 +00:00
98 changed files with 5852 additions and 1057 deletions

5
.gitignore vendored
View File

@@ -1,3 +1,8 @@
*.egg-info
*.pyc
.tox
/build
/dist
/docs/_site
/venv
fig.spec

View File

@@ -1,11 +0,0 @@
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
install:
- python setup.py install
- pip install nose==1.3.0
script: nosetests

233
CHANGES.md Normal file
View File

@@ -0,0 +1,233 @@
Change log
==========
1.0.0 (2014-10-07)
------------------
The highlights:
- [Fig has joined Docker.](https://www.orchardup.com/blog/orchard-is-joining-docker) Fig will continue to be maintained, but we'll also be incorporating the best bits of Fig into Docker itself.
This means the GitHub repository has moved to [https://github.com/docker/fig](https://github.com/docker/fig) and our IRC channel is now #docker-fig on Freenode.
- Fig can be used with the [official Docker OS X installer](https://docs.docker.com/installation/mac/). Boot2Docker will mount the home directory from your host machine so volumes work as expected.
- Fig supports Docker 1.3.
- It is now possible to connect to the Docker daemon using TLS by using the `DOCKER_CERT_PATH` and `DOCKER_TLS_VERIFY` environment variables.
- There is a new `fig port` command which outputs the host port binding of a service, in a similar way to `docker port`.
- There is a new `fig pull` command which pulls the latest images for a service.
- There is a new `fig restart` command which restarts a service's containers.
- Fig creates multiple containers in service by appending a number to the service name (e.g. `db_1`, `db_2`, etc). As a convenience, Fig will now give the first container an alias of the service name (e.g. `db`).
This link alias is also a valid hostname and added to `/etc/hosts` so you can connect to linked services using their hostname. For example, instead of resolving the environment variables `DB_PORT_5432_TCP_ADDR` and `DB_PORT_5432_TCP_PORT`, you could just use the hostname `db` and port `5432` directly.
- Volume definitions now support `ro` mode, expanding `~` and expanding environment variables.
- `.dockerignore` is supported when building.
- The project name can be set with the `FIG_PROJECT_NAME` environment variable.
- The `--env` and `--entrypoint` options have been added to `fig run`.
- The Fig binary for Linux is now linked against an older version of glibc so it works on CentOS 6 and Debian Wheezy.
Other things:
- `fig ps` now works on Jenkins and makes fewer API calls to the Docker daemon.
- `--verbose` displays more useful debugging output.
- When starting a service where `volumes_from` points to a service without any containers running, that service will now be started.
- Lots of docs improvements. Notably, environment variables are documented and official repositories are used throughout.
0.5.2 (2014-07-28)
------------------
- Added a `--no-cache` option to `fig build`, which bypasses the cache just like `docker build --no-cache`.
- Fixed the `dns:` fig.yml option, which was causing fig to error out.
- Fixed a bug where fig couldn't start under Python 2.6.
- Fixed a log-streaming bug that occasionally caused fig to exit.
Thanks @dnephin and @marksteve!
0.5.1 (2014-07-11)
------------------
- If a service has a command defined, `fig run [service]` with no further arguments will run it.
- The project name now defaults to the directory containing fig.yml, not the current working directory (if they're different)
- `volumes_from` now works properly with containers as well as services
- Fixed a race condition when recreating containers in `fig up`
Thanks @ryanbrainard and @d11wtq!
0.5.0 (2014-07-11)
------------------
- Fig now starts links when you run `fig run` or `fig up`.
For example, if you have a `web` service which depends on a `db` service, `fig run web ...` will start the `db` service.
- Environment variables can now be resolved from the environment that Fig is running in. Just specify it as a blank variable in your `fig.yml` and, if set, it'll be resolved:
```
environment:
RACK_ENV: development
SESSION_SECRET:
```
- `volumes_from` is now supported in `fig.yml`. All of the volumes from the specified services and containers will be mounted:
```
volumes_from:
- service_name
- container_name
```
- A host address can now be specified in `ports`:
```
ports:
- "0.0.0.0:8000:8000"
- "127.0.0.1:8001:8001"
```
- The `net` and `workdir` options are now supported in `fig.yml`.
- The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option.
- TTY behaviour is far more robust, and resizes are supported correctly.
- Load YAML files safely.
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca, @mozz100 and @marksteve for their help with this release!
0.4.2 (2014-06-18)
------------------
- Fix various encoding errors when using `fig run`, `fig up` and `fig build`.
0.4.1 (2014-05-08)
------------------
- Add support for Docker 0.11.0. (Thanks @marksteve!)
- Make project name configurable. (Thanks @jefmathiot!)
- Return correct exit code from `fig run`.
0.4.0 (2014-04-29)
------------------
- Support Docker 0.9 and 0.10
- Display progress bars correctly when pulling images (no more ski slopes)
- `fig up` now stops all services when any container exits
- Added support for the `privileged` config option in fig.yml (thanks @kvz!)
- Shortened and aligned log prefixes in `fig up` output
- Only containers started with `fig run` link back to their own service
- Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!)
- Error message improvements
0.3.2 (2014-03-05)
------------------
- Added an `--rm` option to `fig run`. (Thanks @marksteve!)
- Added an `expose` option to `fig.yml`.
0.3.1 (2014-03-04)
------------------
- Added contribution instructions. (Thanks @kvz!)
- Fixed `fig rm` throwing an error.
- Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
0.3.0 (2014-03-03)
------------------
- We now ship binaries for OS X and Linux. No more having to install with Pip!
- Add `-f` flag to specify alternate `fig.yml` files
- Add support for custom link names
- Fix a bug where recreating would sometimes hang
- Update docker-py to support Docker 0.8.0.
- Various documentation improvements
- Various error message improvements
Thanks @marksteve, @Gazler and @teozkr!
0.2.2 (2014-02-17)
------------------
- Resolve dependencies using Cormen/Tarjan topological sort
- Fix `fig up` not printing log output
- Stop containers in reverse order to starting
- Fix scale command not binding ports
Thanks to @barnybug and @dustinlacewell for their work on this release.
0.2.1 (2014-02-04)
------------------
- General improvements to error reporting (#77, #79)
0.2.0 (2014-01-31)
------------------
- Link services to themselves so run commands can access the running service. (#67)
- Much better documentation.
- Make service dependency resolution more reliable. (#48)
- Load Fig configurations with a `.yaml` extension. (#58)
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
0.1.4 (2014-01-27)
------------------
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
0.1.3 (2014-01-23)
------------------
- Fix ports sometimes being configured incorrectly. (#46)
- Fix log output sometimes not displaying. (#47)
0.1.2 (2014-01-22)
------------------
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
0.1.1 (2014-01-17)
------------------
- Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell!
0.1.0 (2014-01-16)
------------------
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
- Add `fig scale` command (#9)
- Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19)
- Truncate long commands in `fig ps` (#18)
- Fill out CLI help banners for commands (#15, #16)
- Show a friendlier error when `fig.yml` is missing (#4)
- Fix bug with `fig build` logging (#3)
- Fix bug where builds would time out if a step took a long time without generating output (#6)
- Fix bug where streaming container output over the Unix socket raised an error (#7)
Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt.
0.0.2 (2014-01-02)
------------------
- Improve documentation
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
- Improve `fig up` behaviour
- Add confirmation prompt to `fig rm`
- Add `fig build` command
0.0.1 (2013-12-20)
------------------
Initial release.

98
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,98 @@
# Contributing to Fig
## Development environment
If you're looking contribute to [Fig](http://www.fig.sh/)
but you're new to the project or maybe even to Python, here are the steps
that should get you started.
1. Fork [https://github.com/docker/fig](https://github.com/docker/fig) to your username. kvz in this example.
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
1. Enter the local directory `cd fig`.
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
1. Works for you? Run the test suite via `./script/test` to verify it won't break other usecases.
1. All good? Commit and push to GitHub, and submit a pull request.
## Running the test suite
$ script/test
## Building binaries
Linux:
$ script/build-linux
OS X:
$ script/build-osx
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
## Sign your work
The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right to
pass it on as an open-source patch. The rules are pretty simple: if you
can certify the below (from [developercertificate.org](http://developercertificate.org/)):
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
then you just add a line saying
Signed-off-by: Random J Developer <random@developer.example.org>
using your real name (sorry, no pseudonyms or anonymous contributions.)
The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
$ git commit --signoff
## Release process
1. Open pull request that:
- Updates version in `fig/__init__.py`
- Updates version in `docs/install.md`
- Adds release notes to `CHANGES.md`
2. Create unpublished GitHub release with release notes
3. Build Linux version on any Docker host with `script/build-linux` and attach to release
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release as `fig-Darwin-x86_64` and `fig-Linux-x86_64`.
5. Publish GitHub release, creating tag
6. Update website with `script/deploy-docs`
7. Upload PyPi package
$ git checkout $VERSION
$ python setup.py sdist upload

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM debian:wheezy
RUN apt-get update -qq && apt-get install -qy python python-pip python-dev git && apt-get clean
RUN useradd -d /home/user -m -s /bin/bash user
WORKDIR /code/
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD requirements-dev.txt /code/
RUN pip install -r requirements-dev.txt
ADD . /code/
RUN python setup.py install
RUN chown -R user /code/

215
LICENSE
View File

@@ -1,24 +1,191 @@
Copyright (c) 2013, Orchard Laboratories Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2014 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

4
MAINTAINERS Normal file
View File

@@ -0,0 +1,4 @@
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
Chris Corbyn <chris@w3style.co.uk> (@d11wtq)
Nathan LeClaire <nathan.leclaire@gmail.com> (@nathanleclaire)

View File

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

167
README.md
View File

@@ -1,163 +1,46 @@
Fig
====
===
Punctual, lightweight development environments using Docker.
[![wercker status](https://app.wercker.com/status/d5dbac3907301c3d5ce735e2d5e95a5b/s/master "wercker status")](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b)
Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this:
Fast, isolated development environments using Docker.
```yaml
web:
build: .
links:
- db
ports:
- 8000:8000
db:
image: orchardup/postgresql
```
Define your app's environment with Docker so it can be reproduced anywhere:
Installing
----------
```bash
$ sudo pip install fig
```
Defining your app
-----------------
Put a `fig.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use.
The simplest way to get started is to just give it an image name:
```yaml
db:
image: orchardup/postgresql
```
You've now given Fig the minimal amount of configuration it needs to run:
```bash
$ fig up
Pulling image orchardup/postgresql...
Starting myapp_db_1...
myapp_db_1 is running at 127.0.0.1:45678
<...output from postgresql server...>
```
For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`.
By default, `fig up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag:
```bash
$ fig up -d
Starting myapp_db_1... done
myapp_db_1 is running at 127.0.0.1:45678
$ fig ps
Name State Ports
------------------------------------
myapp_db_1 Up 5432->45678/tcp
```
### Building services
Fig can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command).
This example will build an image with `app.py` inside it:
#### app.py
```python
print "Hello world!"
```
#### fig.yml
```yaml
web:
build: .
```
#### Dockerfile
FROM ubuntu:12.04
RUN apt-get install python
ADD . /opt
WORKDIR /opt
FROM python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD python app.py
### Getting your code in
If you want to work on an application being run by Fig, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately:
Define the services that make up your app so they can be run together in an isolated environment:
```yaml
web:
build: .
volumes:
- .:/opt
```
### Communicating between containers
Your web app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables:
```yaml
db:
image: orchardup/postgresql
web:
build: .
links:
- db
```
This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container:
```bash
$ fig up -d db
$ fig run web env
```
### Container configuration options
You can pass extra configuration options to a container, much like with `docker run`:
```yaml
web:
build: .
-- override the default command
command: bundle exec thin -p 3000
-- expose ports, optionally specifying both host and container ports (a random host port will be chosen otherwise)
ports:
- 3000
- 8000:8000
-- map volumes
volumes:
- cache/:/tmp/cache
-- add environment variables
environment:
RACK_ENV: development
- "8000:8000"
- "49100:22"
db:
image: postgres
```
(No more installing Postgres on your laptop!)
Running a one-off command
-------------------------
Then type `fig up`, and Fig will start and run your entire app:
If you want to run a management command, use `fig run` to start a one-off container:
![example fig run](https://orchardup.com/static/images/fig-example-large.gif)
```bash
$ fig run db createdb myapp_development
$ fig run web rake db:migrate
$ fig run web bash
```
There are commands to:
- start, stop and rebuild services
- view the status of running services
- tail running services' log output
- run a one-off command on a service
Installation and documentation
------------------------------
Full documentation is available on [Fig's website](http://www.fig.sh/).

3
bin/fig Executable file
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

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
www.fig.sh

10
docs/Dockerfile Normal file
View File

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

3
docs/Gemfile Normal file
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,71 @@
<!DOCTYPE html>
<html lang="en-gb">
<head>
<meta charset="utf-8">
<title>{{ page.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='http://fonts.googleapis.com/css?family=Lilita+One|Lato:300,400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="css/fig.css?{{ site.time | date:'%Y%m%d%U%H%N%S' }}">
<link rel="canonical" href="http://www.fig.sh{% if page.url =="/index.html" %}/{% else %}{{ page.url }}{% endif %}">
</head>
<body>
<div class="container">
<div class="logo mobile-logo">
<a href="index.html">
<img src="img/logo.png">
Fig
</a>
</div>
<div class="content">{{ content }}</div>
<div class="sidebar">
<h1 class="logo">
<a href="index.html">
<img src="img/logo.png">
Fig
</a>
</h1>
<ul class="nav">
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Install</a></li>
<li><a href="rails.html">Get started with Rails</a></li>
<li><a href="django.html">Get started with Django</a></li>
<li><a href="wordpress.html">Get started with Wordpress</a></li>
</ul>
<ul class="nav">
<li>Reference:</li>
<ul>
<li><a href="yml.html">fig.yml</a></li>
<li><a href="cli.html">Commands</a></li>
<li><a href="env.html">Environment variables</a></li>
</ul>
</ul>
<ul class="nav">
<li><a href="https://github.com/docker/fig">Fig on GitHub</a></li>
<li><a href="http://webchat.freenode.net/?channels=%23docker-fig&uio=d4">#docker-fig on Freenode</a></li>
</ul>
<p>Fig is a project from <a href="https://www.docker.com">Docker</a>.</p>
<div class="badges">
<iframe src="http://ghbtns.com/github-btn.html?user=docker&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>

124
docs/cli.md Normal file
View File

@@ -0,0 +1,124 @@
---
layout: default
title: Fig CLI reference
---
CLI reference
=============
Most commands are run against one or more services. If the service is omitted, it will apply to all services.
Run `fig [COMMAND] --help` for full usage.
## Commands
### build
Build or rebuild services.
Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it.
### help
Get help on a command.
### kill
Force stop service containers.
### logs
View output from services.
### port
Print the public port for a port binding
### ps
List containers.
### pull
Pulls service images.
### rm
Remove stopped service containers.
### run
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
By default, linked services will be started, unless they are already running.
One-off commands are started in new containers with the same config as a normal container for that service, so volumes, links, etc will all be created as expected. The only thing different to a normal container is the command will be overridden with the one specified and no ports will be created in case they collide.
Links are also created between one-off commands and the other containers for that service so you can do stuff like this:
$ fig run db psql -h db -U docker
If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag:
$ fig run --no-deps web python manage.py shell
### scale
Set number of containers to run for a service.
Numbers are specified in the form `service=num` as arguments.
For example:
$ fig scale web=2 worker=3
### start
Start existing containers for a service.
### stop
Stop running containers without removing them. They can be started again with `fig start`.
### up
Build, (re)create, start and attach to containers for a service.
Linked services will be started, unless they are already running.
By default, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them running.
By default if there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up. If you do no want containers to be stopped and recreated, use `fig up --no-recreate`. This will still start any stopped containers, if needed.
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
## Environment Variables
Several environment variables can be used to configure Fig's behaviour.
Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values.
### FIG\_PROJECT\_NAME
Set the project name, which is prepended to the name of every container started by Fig. Defaults to the `basename` of the current working directory.
### FIG\_FILE
Set the path to the `fig.yml` to use. Defaults to `fig.yml` in the current working directory.
### DOCKER\_HOST
Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as with the docker client.
### DOCKER\_TLS\_VERIFY
When set to anything other than an empty string, enables TLS communication with the daemon.
### DOCKER\_CERT\_PATH
Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`.

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

File diff suppressed because one or more lines are too long

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

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

91
docs/django.md Normal file
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 python:2.7
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/
That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
Second, we define our Python dependencies in a file called `requirements.txt`:
Django
psycopg2
Simple enough. Finally, this is all tied together with a file called `fig.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.
db:
image: postgres
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
links:
- db
See the [`fig.yml` reference](yml.html) for more information on how it works.
We can now start a Django project using `fig run`:
$ fig run web django-admin.py startproject figexample .
First, Fig will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject figexample .` inside a container using that image.
This will generate a Django app inside the current directory:
$ ls
Dockerfile fig.yml figexample manage.py requirements.txt
First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `figexample/settings.py` to read:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'postgres',
'USER': 'postgres',
'HOST': 'db',
'PORT': 5432,
}
}
These settings are determined by the [postgres](https://registry.hub.docker.com/_/postgres/) Docker image we are using.
Then, run `fig up`:
Recreating myapp_db_1...
Recreating myapp_web_1...
Attaching to myapp_db_1, myapp_web_1
myapp_db_1 |
myapp_db_1 | PostgreSQL stand-alone backend 9.1.11
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: database system is ready to accept connections
myapp_db_1 | 2014-01-27 12:17:03 UTC LOG: autovacuum launcher started
myapp_web_1 | Validating models...
myapp_web_1 |
myapp_web_1 | 0 errors found
myapp_web_1 | January 27, 2014 - 12:12:40
myapp_web_1 | Django version 1.6.1, using settings 'figexample.settings'
myapp_web_1 | Starting development server at http://0.0.0.0:8000/
myapp_web_1 | Quit the server with CONTROL-C.
And your Django app should be running at port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).
You can also run management commands with Docker. To set up your database, for example, run `fig up` and in another terminal run:
$ fig run web python manage.py syncdb

33
docs/env.md Normal file
View File

@@ -0,0 +1,33 @@
---
layout: default
title: Fig environment variables reference
---
Environment variables reference
===============================
**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [fig.yml documentation](yml.html#links) for details.
Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
To see what environment variables are available to a service, run `fig run SERVICE env`.
<b><i>name</i>\_PORT</b><br>
Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
Full URL, e.g. `DB_PORT_5432_TCP=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
Container's IP address, e.g. `DB_PORT_5432_TCP_ADDR=172.17.0.5`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
Exposed port number, e.g. `DB_PORT_5432_TCP_PORT=5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp`
<b><i>name</i>\_NAME</b><br>
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
[Docker links]: http://docs.docker.com/userguide/dockerlinks/

8
docs/fig.yml Normal file
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

140
docs/index.md Normal file
View File

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

27
docs/install.md Normal file
View File

@@ -0,0 +1,27 @@
---
layout: default
title: Installing Fig
---
Installing Fig
==============
First, install Docker version 1.3 or greater.
If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll configure Docker and Fig to talk to it:
$(boot2docker shellinit)
To persist the environment variables across shell sessions, you can add that line to your `~/.bashrc` file.
There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) and [other platforms](https://docs.docker.com/installation/) in Dockers documentation.
Next, install Fig:
curl -L https://github.com/docker/fig/releases/download/1.0.0/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig
Releases are available for OS X and 64-bit Linux. Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
$ sudo pip install -U fig
That should be all you need! Run `fig --version` to see if it worked.

98
docs/rails.md Normal file
View File

@@ -0,0 +1,98 @@
---
layout: default
title: Getting started with Fig and Rails
---
Getting started with Fig and Rails
==================================
We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html).
Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with:
FROM ruby
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
RUN bundle install
ADD . /myapp
That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
source 'https://rubygems.org'
gem 'rails', '4.0.2'
Finally, `fig.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port.
db:
image: postgres
ports:
- "5432"
web:
build: .
command: bundle exec rackup -p 3000
volumes:
- .:/myapp
ports:
- "3000:3000"
links:
- db
With those files in place, we can now generate the Rails skeleton app using `fig run`:
$ fig run web rails new . --force --database=postgresql --skip-bundle
First, Fig will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have a fresh app generated:
$ ls
Dockerfile app fig.yml tmp
Gemfile bin lib vendor
Gemfile.lock config log
README.rdoc config.ru public
Rakefile db test
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've got a Javascript runtime:
gem 'therubyracer', platforms: :ruby
Now that we've got a new `Gemfile`, we need to build the image again. (This, and changes to the Dockerfile itself, should be the only times you'll need to rebuild).
$ fig build
The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on `localhost` - we need to point it at the `db` container instead. We also need to change the database and username to align with the defaults set by the `postgres` image.
Open up your newly-generated `database.yml`. Replace its contents with the following:
development: &default
adapter: postgresql
encoding: unicode
database: postgres
pool: 5
username: postgres
password:
host: db
test:
<<: *default
database: myapp_test
We can now boot the app.
$ fig up
If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain:
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu]
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000
Finally, we just need to create the database. In another terminal, run:
$ fig run web rake db:create
And we're rolling—your app should now be running on port 3000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).
![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 [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
Next up, `fig.yml` starts our web service and a separate MySQL instance:
```
web:
build: .
command: php -S 0.0.0.0:8000 -t /code
ports:
- "8000:8000"
links:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
environment:
MYSQL_DATABASE: wordpress
```
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
```
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', "db:3306");
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
```
Finally, `router.php` tells PHP's built-in web server how to run Wordpress:
```
<?php
$root = $_SERVER['DOCUMENT_ROOT'];
chdir($root);
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
set_include_path(get_include_path().':'.__DIR__);
if(file_exists($root.$path))
{
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
$path = rtrim($path,'/').'/index.php';
if(strpos($path,'.php') === false) return false;
else {
chdir(dirname($root.$path));
require_once $root.$path;
}
}else include_once 'index.php';
```
With those four files in place, run `fig up` inside your Wordpress directory and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress at port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address).

159
docs/yml.md Normal file
View File

@@ -0,0 +1,159 @@
---
layout: default
title: fig.yml reference
---
fig.yml reference
=================
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
###image
Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
```
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
```
### build
Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
```
build: /path/to/build/dir
```
### command
Override the default command.
```
command: bundle exec thin -p 3000
```
<a name="links"></a>
### links
Link to containers in another service. Either specify both the service name and the link alias (`SERVICE:ALIAS`), or just the service name (which will also be used for the alias).
```
links:
- db
- db:database
- redis
```
An entry with the alias' name will be created in `/etc/hosts` inside containers for this service, e.g:
```
172.17.2.186 db
172.17.2.186 database
172.17.2.187 redis
```
Environment variables will also be created - see the [environment variable reference](env.html) for details.
### ports
Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).
**Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings.
```
ports:
- "3000"
- "8000:8000"
- "49100:22"
- "127.0.0.1:8001:8001"
```
### expose
Expose ports without publishing them to the host machine - they'll only be accessible to linked services. Only the internal port can be specified.
```
expose:
- "3000"
- "8000"
```
### volumes
Mount paths as volumes, optionally specifying a path on the host machine
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
```
volumes:
- /var/lib/mysql
- cache/:/tmp/cache
- ~/configs:/etc/configs/:ro
```
### volumes_from
Mount all of the volumes from another service or container.
```
volumes_from:
- service_name
- container_name
```
### environment
Add environment variables. You can use either an array or a dictionary.
Environment variables with only a key are resolved to their values on the machine Fig is running on, which can be helpful for secret or host-specific values.
```
environment:
RACK_ENV: development
SESSION_SECRET:
environment:
- RACK_ENV=development
- SESSION_SECRET
```
### net
Networking mode. Use the same values as the docker client `--net` parameter.
```
net: "bridge"
net: "none"
net: "container:[name or id]"
net: "host"
```
### dns
Custom DNS servers. Can be a single value or a list.
```
dns: 8.8.8.8
dns:
- 8.8.8.8
- 9.9.9.9
```
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged
Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart.
```
working_dir: /code
entrypoint: /code/entrypoint.sh
user: postgresql
hostname: foo
domainname: foo.com
mem_limit: 1000000000
privileged: true
```

View File

@@ -1,3 +1,4 @@
from .service import Service
from __future__ import unicode_literals
from .service import Service # noqa:flake8
__version__ = '0.0.1'
__version__ = '1.0.0'

View File

@@ -1,3 +1,4 @@
from __future__ import unicode_literals
NAMES = [
'grey',
'red',

View File

@@ -1,38 +1,109 @@
from docker import Client
from __future__ import unicode_literals
from __future__ import absolute_import
from requests.exceptions import ConnectionError, SSLError
import errno
import logging
import os
import re
import yaml
import six
from ..project import Project
from ..service import ConfigError
from .docopt_command import DocoptCommand
from .formatter import Formatter
from .utils import cached_property, mkdir
from .utils import call_silently, is_mac, is_ubuntu
from .docker_client import docker_client
from . import verbose_proxy
from . import errors
from .. import __version__
log = logging.getLogger(__name__)
class Command(DocoptCommand):
@cached_property
def client(self):
if os.environ.get('DOCKER_URL'):
return Client(os.environ['DOCKER_URL'])
else:
return Client()
base_dir = '.'
@cached_property
def project(self):
config = yaml.load(open('fig.yml'))
return Project.from_config(self.project_name, config, self.client)
def dispatch(self, *args, **kwargs):
try:
super(Command, self).dispatch(*args, **kwargs)
except SSLError, e:
raise errors.UserError('SSL error: %s' % e)
except ConnectionError:
if call_silently(['which', 'docker']) != 0:
if is_mac():
raise errors.DockerNotFoundMac()
elif is_ubuntu():
raise errors.DockerNotFoundUbuntu()
else:
raise errors.DockerNotFoundGeneric()
elif call_silently(['which', 'boot2docker']) == 0:
raise errors.ConnectionErrorBoot2Docker()
else:
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
@cached_property
def project_name(self):
project = os.path.basename(os.getcwd())
project = re.sub(r'[^a-zA-Z0-9]', '', project)
if not project:
project = 'default'
return project
def perform_command(self, options, handler, command_options):
explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE')
project = self.get_project(
self.get_config_path(explicit_config_path),
project_name=options.get('--project-name'),
verbose=options.get('--verbose'))
@cached_property
def formatter(self):
return Formatter()
handler(project, command_options)
def get_client(self, verbose=False):
client = docker_client()
if verbose:
version_info = six.iteritems(client.version())
log.info("Fig version %s", __version__)
log.info("Docker base_url: %s", client.base_url)
log.info("Docker version: %s",
", ".join("%s=%s" % item for item in version_info))
return verbose_proxy.VerboseProxy('docker', client)
return client
def get_config(self, config_path):
try:
with open(config_path, 'r') as fh:
return yaml.safe_load(fh)
except IOError as e:
if e.errno == errno.ENOENT:
raise errors.FigFileNotFound(os.path.basename(e.filename))
raise errors.UserError(six.text_type(e))
def get_project(self, config_path, project_name=None, verbose=False):
try:
return Project.from_config(
self.get_project_name(config_path, project_name),
self.get_config(config_path),
self.get_client(verbose=verbose))
except ConfigError as e:
raise errors.UserError(six.text_type(e))
def get_project_name(self, config_path, project_name=None):
def normalize_name(name):
return re.sub(r'[^a-zA-Z0-9]', '', name)
project_name = project_name or os.environ.get('FIG_PROJECT_NAME')
if project_name is not None:
return normalize_name(project_name)
project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
if project:
return normalize_name(project)
return 'default'
def get_config_path(self, file_path=None):
if file_path:
return os.path.join(self.base_dir, file_path)
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
log.warning("Fig just read the file 'fig.yaml' on startup, rather "
"than 'fig.yml'")
log.warning("Please be aware that fig.yml the expected extension "
"in most cases, and using .yaml can cause compatibility "
"issues in future")
return os.path.join(self.base_dir, 'fig.yaml')
return os.path.join(self.base_dir, 'fig.yml')

34
fig/cli/docker_client.py Normal file
View File

@@ -0,0 +1,34 @@
from docker import Client
from docker import tls
import ssl
import os
def docker_client():
"""
Returns a docker-py client configured using environment variables
according to the same logic as the official Docker client.
"""
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
if cert_path == '':
cert_path = os.path.join(os.environ.get('HOME'), '.docker')
base_url = os.environ.get('DOCKER_HOST')
tls_config = None
if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
parts = base_url.split('://', 1)
base_url = '%s://%s' % ('https', parts[1])
client_cert = (os.path.join(cert_path, 'cert.pem'), os.path.join(cert_path, 'key.pem'))
ca_cert = os.path.join(cert_path, 'ca.pem')
tls_config = tls.TLSConfig(
ssl_version=ssl.PROTOCOL_TLSv1,
verify=True,
assert_hostname=False,
client_cert=client_cert,
ca_cert=ca_cert,
)
return Client(base_url=base_url, tls=tls_config)

View File

@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import sys
from inspect import getdoc
@@ -21,7 +23,7 @@ class DocoptCommand(object):
def dispatch(self, argv, global_options):
self.perform_command(*self.parse(argv, global_options))
def perform_command(self, options, command, handler, command_options):
def perform_command(self, options, handler, command_options):
handler(command_options)
def parse(self, argv, global_options):
@@ -41,7 +43,7 @@ class DocoptCommand(object):
raise NoSuchCommand(command, self)
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
return (options, command, handler, command_options)
return options, handler, command_options
class NoSuchCommand(Exception):

View File

@@ -1,6 +1,62 @@
from __future__ import absolute_import
from textwrap import dedent
class UserError(Exception):
def __init__(self, msg):
self.msg = dedent(msg).strip()
def __unicode__(self):
return self.msg
__str__ = __unicode__
class DockerNotFoundMac(UserError):
def __init__(self):
super(DockerNotFoundMac, self).__init__("""
Couldn't connect to Docker daemon. You might need to install docker-osx:
https://github.com/noplay/docker-osx
""")
class DockerNotFoundUbuntu(UserError):
def __init__(self):
super(DockerNotFoundUbuntu, self).__init__("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/ubuntulinux/
""")
class DockerNotFoundGeneric(UserError):
def __init__(self):
super(DockerNotFoundGeneric, self).__init__("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/
""")
class ConnectionErrorBoot2Docker(UserError):
def __init__(self):
super(ConnectionErrorBoot2Docker, self).__init__("""
Couldn't connect to Docker daemon - you might need to run `boot2docker up`.
""")
class ConnectionErrorGeneric(UserError):
def __init__(self, url):
super(ConnectionErrorGeneric, self).__init__("""
Couldn't connect to Docker daemon at %s - is it running?
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
""" % url)
class FigFileNotFound(UserError):
def __init__(self, filename):
super(FigFileNotFound, self).__init__("""
Can't find %s. Are you in the right directory?
""" % filename)

View File

@@ -1,12 +1,20 @@
import texttable
from __future__ import unicode_literals
from __future__ import absolute_import
import os
import texttable
def get_tty_width():
tty_size = os.popen('stty size', 'r').read().split()
if len(tty_size) != 2:
return 80
_, width = tty_size
return int(width)
class Formatter(object):
def table(self, headers, rows):
height, width = os.popen('stty size', 'r').read().split()
table = texttable.Texttable(max_width=width)
table = texttable.Texttable(max_width=get_tty_width())
table.set_cols_dtype(['t' for h in headers])
table.add_rows([headers] + rows)
table.set_deco(table.HEADER)

View File

@@ -1,66 +1,79 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import sys
from itertools import cycle
from .multiplexer import Multiplexer
from .multiplexer import Multiplexer, STOP
from . import colors
from .utils import split_buffer
class LogPrinter(object):
def __init__(self, containers, attach_params=None):
def __init__(self, containers, attach_params=None, output=sys.stdout, monochrome=False):
self.containers = containers
self.attach_params = attach_params or {}
self.generators = self._make_log_generators()
self.prefix_width = self._calculate_prefix_width(containers)
self.generators = self._make_log_generators(monochrome)
self.output = output
def run(self):
mux = Multiplexer(self.generators)
for line in mux.loop():
sys.stdout.write(line)
self.output.write(line)
def _make_log_generators(self):
def _calculate_prefix_width(self, containers):
"""
Calculate the maximum width of container names so we can make the log
prefixes line up like so:
db_1 | Listening
web_1 | Listening
"""
prefix_width = 0
for container in containers:
prefix_width = max(prefix_width, len(container.name_without_project))
return prefix_width
def _make_log_generators(self, monochrome):
color_fns = cycle(colors.rainbow())
generators = []
for container in self.containers:
color_fn = color_fns.next()
if monochrome:
color_fn = lambda s: s
else:
color_fn = color_fns.next()
generators.append(self._make_log_generator(container, color_fn))
return generators
def _make_log_generator(self, container, color_fn):
prefix = color_fn(container.name + " | ")
websocket = self._attach(container)
return (prefix + line for line in split_buffer(read_websocket(websocket), '\n'))
prefix = color_fn(self._generate_prefix(container)).encode('utf-8')
# Attach to container before log printer starts running
line_generator = split_buffer(self._attach(container), '\n')
for line in line_generator:
yield prefix + line
exit_code = container.wait()
yield color_fn("%s exited with code %s\n" % (container.name, exit_code))
yield STOP
def _generate_prefix(self, container):
"""
Generate the prefix for a log line without colour
"""
name = container.name_without_project
padding = ' ' * (self.prefix_width - len(name))
return ''.join([name, padding, ' | '])
def _attach(self, container):
params = {
'stdin': False,
'stdout': True,
'stderr': True,
'logs': False,
'stream': True,
}
params.update(self.attach_params)
params = dict((name, 1 if value else 0) for (name, value) in params.items())
return container.attach_socket(params=params, ws=True)
def read_websocket(websocket):
while True:
data = websocket.recv()
if data:
yield data
else:
break
def split_buffer(reader, separator):
buffered = ''
for data in reader:
lines = (buffered + data).split(separator)
for line in lines[:-1]:
yield line + separator
if len(lines) > 1:
buffered = lines[-1]
if len(buffered) > 0:
yield buffered
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
return container.attach(**params)

View File

@@ -1,25 +1,55 @@
from __future__ import print_function
from __future__ import unicode_literals
import logging
import sys
import re
import signal
from operator import attrgetter
from inspect import getdoc
from fig.packages import dockerpty
from .. import __version__
from ..project import NoSuchService
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError
from .command import Command
from .formatter import Formatter
from .log_printer import LogPrinter
from .utils import yesno
from docker.client import APIError
from docker.errors import APIError
from .errors import UserError
from .docopt_command import NoSuchCommand
from .socketclient import SocketClient
log = logging.getLogger(__name__)
def main():
console_handler = logging.StreamHandler()
setup_logging()
try:
command = TopLevelCommand()
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
sys.exit(1)
except (UserError, NoSuchService, ConfigurationError) as e:
log.error(e.msg)
sys.exit(1)
except NoSuchCommand as e:
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
sys.exit(1)
except APIError as e:
log.error(e.explanation)
sys.exit(1)
except BuildError as e:
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
sys.exit(1)
def setup_logging():
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(logging.Formatter())
console_handler.setLevel(logging.INFO)
root_logger = logging.getLogger()
@@ -29,27 +59,6 @@ def main():
# Disable requests logging
logging.getLogger("requests").propagate = False
try:
command = TopLevelCommand()
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except UserError, e:
log.error(e.msg)
exit(1)
except NoSuchService, e:
log.error(e.msg)
exit(1)
except NoSuchCommand, e:
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
exit(1)
except APIError, e:
log.error(e.explanation)
exit(1)
# stolen from docopt master
def parse_doc_section(name, source):
@@ -66,18 +75,26 @@ class TopLevelCommand(Command):
fig -h|--help
Options:
--verbose Show more output
--version Print version and exit
--verbose Show more output
--version Print version and exit
-f, --file FILE Specify an alternate fig file (default: fig.yml)
-p, --project-name NAME Specify an alternate project name (default: directory name)
Commands:
up Create and start containers
build Build or rebuild services
help Get help on a command
kill Kill containers
logs View output from containers
port Print the public port for a port binding
ps List containers
pull Pulls service images
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
kill Kill containers
rm Remove stopped containers
restart Restart services
up Create and start containers
"""
def docopt_options(self):
@@ -85,7 +102,77 @@ class TopLevelCommand(Command):
options['version'] = "fig %s" % __version__
return options
def ps(self, options):
def build(self, project, options):
"""
Build or rebuild services.
Services are built once and then tagged as `project_service`,
e.g. `figtest_db`. If you change a service's `Dockerfile` or the
contents of its build directory, you can run `fig build` to rebuild it.
Usage: build [options] [SERVICE...]
Options:
--no-cache Do not use cache when building the image.
"""
no_cache = bool(options.get('--no-cache', False))
project.build(service_names=options['SERVICE'], no_cache=no_cache)
def help(self, project, options):
"""
Get help on a command.
Usage: help COMMAND
"""
command = options['COMMAND']
if not hasattr(self, command):
raise NoSuchCommand(command, self)
raise SystemExit(getdoc(getattr(self, command)))
def kill(self, project, options):
"""
Force stop service containers.
Usage: kill [SERVICE...]
"""
project.kill(service_names=options['SERVICE'])
def logs(self, project, options):
"""
View output from containers.
Usage: logs [options] [SERVICE...]
Options:
--no-color Produce monochrome output.
"""
containers = project.containers(service_names=options['SERVICE'], stopped=True)
monochrome = options['--no-color']
print("Attaching to", list_containers(containers))
LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
def port(self, project, options):
"""
Print the public port for a port binding.
Usage: port [options] SERVICE PRIVATE_PORT
Options:
--protocol=proto tcp or udp (defaults to tcp)
--index=index index of the container if there are multiple
instances of a service (defaults to 1)
"""
service = project.get_service(options['SERVICE'])
try:
container = service.get_container(number=options.get('--index') or 1)
except ValueError as e:
raise UserError(str(e))
print(container.get_local_port(
options['PRIVATE_PORT'],
protocol=options.get('--protocol') or 'tcp') or '')
def ps(self, project, options):
"""
List containers.
@@ -94,11 +181,14 @@ class TopLevelCommand(Command):
Options:
-q Only display IDs
"""
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
containers = sorted(
project.containers(service_names=options['SERVICE'], stopped=True) +
project.containers(service_names=options['SERVICE'], one_off=True),
key=attrgetter('name'))
if options['-q']:
for container in containers:
print container.id
print(container.id)
else:
headers = [
'Name',
@@ -108,146 +198,241 @@ class TopLevelCommand(Command):
]
rows = []
for container in containers:
command = container.human_readable_command
if len(command) > 30:
command = '%s ...' % command[:26]
rows.append([
container.name,
container.human_readable_command,
command,
container.human_readable_state,
container.human_readable_ports,
])
print Formatter().table(headers, rows)
print(Formatter().table(headers, rows))
def run(self, options):
def pull(self, project, options):
"""
Run a one-off command.
Pulls images for services.
Usage: run [options] SERVICE COMMAND [ARGS...]
Usage: pull [options] [SERVICE...]
Options:
-d Detached mode: Run container in the background, print new container name
--allow-insecure-ssl Allow insecure connections to the docker
registry
"""
service = self.project.get_service(options['SERVICE'])
insecure_registry = options['--allow-insecure-ssl']
project.pull(
service_names=options['SERVICE'],
insecure_registry=insecure_registry
)
def rm(self, project, options):
"""
Remove stopped service containers.
Usage: rm [options] [SERVICE...]
Options:
--force Don't ask to confirm removal
-v Remove volumes associated with containers
"""
all_containers = project.containers(service_names=options['SERVICE'], stopped=True)
stopped_containers = [c for c in all_containers if not c.is_running]
if len(stopped_containers) > 0:
print("Going to remove", list_containers(stopped_containers))
if options.get('--force') \
or yesno("Are you sure? [yN] ", default=False):
project.remove_stopped(
service_names=options['SERVICE'],
v=options.get('-v', False)
)
else:
print("No stopped containers")
def run(self, project, options):
"""
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
By default, linked services will be started, unless they are already
running. If you do not want to start linked services, use
`fig run --no-deps SERVICE COMMAND [ARGS...]`.
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
Options:
-d Detached mode: Run container in the background, print
new container name.
--entrypoint CMD Override the entrypoint of the image.
-e KEY=VAL Set an environment variable (can be used multiple times)
--no-deps Don't start linked services.
--rm Remove container after run. Ignored in detached mode.
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
"""
service = project.get_service(options['SERVICE'])
if not options['--no-deps']:
deps = service.get_linked_names()
if len(deps) > 0:
project.up(
service_names=deps,
start_links=True,
recreate=False,
)
tty = True
if options['-d'] or options['-T'] or not sys.stdin.isatty():
tty = False
if options['COMMAND']:
command = [options['COMMAND']] + options['ARGS']
else:
command = service.options.get('command')
container_options = {
'command': [options['COMMAND']] + options['ARGS'],
'tty': not options['-d'],
'command': command,
'tty': tty,
'stdin_open': not options['-d'],
}
if options['-e']:
for option in options['-e']:
if 'environment' not in service.options:
service.options['environment'] = {}
k, v = option.split('=', 1)
service.options['environment'][k] = v
if options['--entrypoint']:
container_options['entrypoint'] = options.get('--entrypoint')
container = service.create_container(one_off=True, **container_options)
if options['-d']:
service.start_container(container, ports=None)
print container.name
service.start_container(container, ports=None, one_off=True)
print(container.name)
else:
with self._attach_to_container(
container.id,
interactive=True,
logs=True,
raw=True
) as c:
service.start_container(container, ports=None)
c.run()
service.start_container(container, ports=None, one_off=True)
dockerpty.start(project.client, container.id)
exit_code = container.wait()
if options['--rm']:
log.info("Removing %s..." % container.name)
project.client.remove_container(container.id)
sys.exit(exit_code)
def up(self, options):
def scale(self, project, options):
"""
Create and start containers.
Set number of containers to run for a service.
Usage: up [options] [SERVICE...]
Numbers are specified in the form `service=num` as arguments.
For example:
Options:
-d Detached mode: Run containers in the background, print new container names
$ fig scale web=2 worker=3
Usage: scale [SERVICE=NUM...]
"""
detached = options['-d']
unstarted = self.project.create_containers(service_names=options['SERVICE'])
if not detached:
to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted]
print "Attaching to", list_containers(to_attach)
log_printer = LogPrinter(to_attach, attach_params={'logs': True})
for (s, c) in unstarted:
s.start_container(c)
if detached:
for (s, c) in unstarted:
print c.name
else:
for s in options['SERVICE=NUM']:
if '=' not in s:
raise UserError('Arguments to scale should be in the form service=num')
service_name, num = s.split('=', 1)
try:
log_printer.run()
finally:
self.project.kill_and_remove(unstarted)
num = int(num)
except ValueError:
raise UserError('Number of containers for service "%s" is not a '
'number' % service_name)
try:
project.get_service(service_name).scale(num)
except CannotBeScaledError:
raise UserError(
'Service "%s" cannot be scaled because it specifies a port '
'on the host. If multiple containers for this service were '
'created, the port would clash.\n\nRemove the ":" from the '
'port definition in fig.yml so Docker can choose a random '
'port for each container.' % service_name)
def start(self, options):
def start(self, project, options):
"""
Start existing containers.
Usage: start [SERVICE...]
"""
self.project.start(service_names=options['SERVICE'])
project.start(service_names=options['SERVICE'])
def stop(self, options):
def stop(self, project, options):
"""
Stop running containers.
Stop running containers without removing them.
They can be started again with `fig start`.
Usage: stop [SERVICE...]
"""
self.project.stop(service_names=options['SERVICE'])
project.stop(service_names=options['SERVICE'])
def kill(self, options):
def restart(self, project, options):
"""
Kill containers.
Restart running containers.
Usage: kill [SERVICE...]
Usage: restart [SERVICE...]
"""
self.project.kill(service_names=options['SERVICE'])
project.restart(service_names=options['SERVICE'])
def rm(self, options):
def up(self, project, options):
"""
Remove stopped containers
Build, (re)create, start and attach to containers for a service.
Usage: rm [SERVICE...]
By default, `fig up` will aggregate the output of each container, and
when it exits, all containers will be stopped. If you run `fig up -d`,
it'll start the containers in the background and leave them running.
If there are existing containers for a service, `fig up` will stop
and recreate them (preserving mounted volumes with volumes-from),
so that changes in `fig.yml` are picked up. If you do not want existing
containers to be recreated, `fig up --no-recreate` will re-use existing
containers.
Usage: up [options] [SERVICE...]
Options:
-d Detached mode: Run containers in the background,
print new container names.
--no-color Produce monochrome output.
--no-deps Don't start linked services.
--no-recreate If containers already exist, don't recreate them.
"""
self.project.remove_stopped(service_names=options['SERVICE'])
detached = options['-d']
def logs(self, options):
"""
View output from containers.
monochrome = options['--no-color']
Usage: logs [SERVICE...]
"""
containers = self.project.containers(service_names=options['SERVICE'], stopped=False)
print "Attaching to", list_containers(containers)
LogPrinter(containers, attach_params={'logs': True}).run()
start_links = not options['--no-deps']
recreate = not options['--no-recreate']
service_names = options['SERVICE']
def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False):
stdio = self.client.attach_socket(
container_id,
params={
'stdin': 1 if interactive else 0,
'stdout': 1,
'stderr': 0,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
project.up(
service_names=service_names,
start_links=start_links,
recreate=recreate
)
stderr = self.client.attach_socket(
container_id,
params={
'stdin': 0,
'stdout': 0,
'stderr': 1,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
)
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
if not detached:
print("Attaching to", list_containers(to_attach))
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome)
try:
log_printer.run()
finally:
def handler(signal, frame):
project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
print("Gracefully stopping... (press Ctrl+C again to force)")
project.stop(service_names=service_names)
return SocketClient(
socket_in=stdio,
socket_out=stdio,
socket_err=stderr,
raw=raw,
)
def list_containers(containers):
return ", ".join(c.name for c in containers)

View File

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

View File

@@ -1,129 +0,0 @@
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
from select import select
import sys
import tty
import fcntl
import os
import termios
import threading
import errno
import logging
log = logging.getLogger(__name__)
class SocketClient:
def __init__(self,
socket_in=None,
socket_out=None,
socket_err=None,
raw=True,
):
self.socket_in = socket_in
self.socket_out = socket_out
self.socket_err = socket_err
self.raw = raw
self.stdin_fileno = sys.stdin.fileno()
def __enter__(self):
self.create()
return self
def __exit__(self, type, value, trace):
self.destroy()
def create(self):
if os.isatty(sys.stdin.fileno()):
self.settings = termios.tcgetattr(sys.stdin.fileno())
else:
self.settings = None
if self.socket_in is not None:
self.set_blocking(sys.stdin, False)
self.set_blocking(sys.stdout, True)
self.set_blocking(sys.stderr, True)
if self.raw:
tty.setraw(sys.stdin.fileno())
def set_blocking(self, file, blocking):
fd = file.fileno()
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK)
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
def run(self):
if self.socket_in is not None:
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
recv_threads = []
if self.socket_out is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout)))
if self.socket_err is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr)))
for t in recv_threads:
t.join()
def start_background_thread(self, **kwargs):
thread = threading.Thread(**kwargs)
thread.daemon = True
thread.start()
return thread
def recv_ws(self, socket, stream):
try:
while True:
chunk = socket.recv()
if chunk:
stream.write(chunk)
stream.flush()
else:
break
except Exception, e:
log.debug(e)
def send_ws(self, socket, stream):
while True:
r, w, e = select([stream.fileno()], [], [])
if r:
chunk = stream.read(1)
if chunk == '':
socket.send_close()
break
else:
try:
socket.send(chunk)
except Exception, e:
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
break
else:
raise e
def destroy(self):
if self.settings is not None:
termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings)
sys.stdout.flush()
if __name__ == '__main__':
import websocket
if len(sys.argv) != 2:
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
exit(1)
url = sys.argv[1]
socket = websocket.create_connection(url)
print "connected\r"
with SocketClient(socket, interactive=True) as client:
client.run()

View File

@@ -1,24 +1,10 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from __future__ import division
import datetime
import os
def cached_property(f):
"""
returns a cached property that is calculated by function f
http://code.activestate.com/recipes/576563-cached-property/
"""
def get(self):
try:
return self._property_cache[f]
except AttributeError:
self._property_cache = {}
x = self._property_cache[f] = f(self)
return x
except KeyError:
x = self._property_cache[f] = f(self)
return x
return property(get)
import subprocess
import platform
def yesno(prompt, default=None):
@@ -60,17 +46,58 @@ def prettydate(d):
elif s < 120:
return '1 minute ago'
elif s < 3600:
return '{0} minutes ago'.format(s/60)
return '{0} minutes ago'.format(s / 60)
elif s < 7200:
return '1 hour ago'
else:
return '{0} hours ago'.format(s/3600)
return '{0} hours ago'.format(s / 3600)
def mkdir(path, permissions=0700):
def mkdir(path, permissions=0o700):
if not os.path.exists(path):
os.mkdir(path)
os.chmod(path, permissions)
return path
def split_buffer(reader, separator):
"""
Given a generator which yields strings and a separator string,
joins all input, splits on the separator and yields each chunk.
Unlike string.split(), each chunk includes the trailing
separator, except for the last one if none was found on the end
of the input.
"""
buffered = str('')
separator = str(separator)
for data in reader:
buffered += data
while True:
index = buffered.find(separator)
if index == -1:
break
yield buffered[:index + 1]
buffered = buffered[index + 1:]
if len(buffered) > 0:
yield buffered
def call_silently(*args, **kwargs):
"""
Like subprocess.call(), but redirects stdout and stderr to /dev/null.
"""
with open(os.devnull, 'w') as shutup:
return subprocess.call(*args, stdout=shutup, stderr=shutup, **kwargs)
def is_mac():
return platform.system() == 'Darwin'
def is_ubuntu():
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'

58
fig/cli/verbose_proxy.py Normal file
View File

@@ -0,0 +1,58 @@
import functools
from itertools import chain
import logging
import pprint
import six
def format_call(args, kwargs):
args = (repr(a) for a in args)
kwargs = ("{0!s}={1!r}".format(*item) for item in six.iteritems(kwargs))
return "({0})".format(", ".join(chain(args, kwargs)))
def format_return(result, max_lines):
if isinstance(result, (list, tuple, set)):
return "({0} with {1} items)".format(type(result).__name__, len(result))
if result:
lines = pprint.pformat(result).split('\n')
extra = '\n...' if len(lines) > max_lines else ''
return '\n'.join(lines[:max_lines]) + extra
return result
class VerboseProxy(object):
"""Proxy all function calls to another class and log method name, arguments
and return values for each call.
"""
def __init__(self, obj_name, obj, log_name=None, max_lines=10):
self.obj_name = obj_name
self.obj = obj
self.max_lines = max_lines
self.log = logging.getLogger(log_name or __name__)
def __getattr__(self, name):
attr = getattr(self.obj, name)
if not six.callable(attr):
return attr
return functools.partial(self.proxy_callable, name)
def proxy_callable(self, call_name, *args, **kwargs):
self.log.info("%s %s <- %s",
self.obj_name,
call_name,
format_call(args, kwargs))
result = getattr(self.obj, call_name)(*args, **kwargs)
self.log.info("%s %s -> %s",
self.obj_name,
call_name,
format_return(result, self.max_lines))
return result

View File

@@ -1,10 +1,12 @@
import logging
from __future__ import unicode_literals
from __future__ import absolute_import
import six
log = logging.getLogger(__name__)
class Container(object):
"""
Represents a Docker container, constructed from the output of
Represents a Docker container, constructed from the output of
GET /containers/:id:/json.
"""
def __init__(self, client, dictionary, has_been_inspected=False):
@@ -18,7 +20,7 @@ class Container(object):
Construct a container object from the output of GET /containers/json.
"""
new_dictionary = {
'ID': dictionary['Id'],
'Id': dictionary['Id'],
'Image': dictionary['Image'],
}
for name in dictionary.get('Names', []):
@@ -37,7 +39,11 @@ class Container(object):
@property
def id(self):
return self.dictionary['ID']
return self.dictionary['Id']
@property
def image(self):
return self.dictionary['Image']
@property
def short_id(self):
@@ -48,61 +54,84 @@ class Container(object):
return self.dictionary['Name'][1:]
@property
def human_readable_ports(self):
def name_without_project(self):
return '_'.join(self.dictionary['Name'].split('_')[1:])
@property
def number(self):
try:
return int(self.name.split('_')[-1])
except ValueError:
return None
@property
def ports(self):
self.inspect_if_not_inspected()
if not self.dictionary['NetworkSettings']['Ports']:
return ''
ports = []
for private, public in self.dictionary['NetworkSettings']['Ports'].items():
if public:
ports.append('%s->%s' % (public[0]['HostPort'], private))
return ', '.join(ports)
return self.get('NetworkSettings.Ports') or {}
@property
def human_readable_ports(self):
def format_port(private, public):
if not public:
return private
return '{HostIp}:{HostPort}->{private}'.format(
private=private, **public[0])
return ', '.join(format_port(*item)
for item in sorted(six.iteritems(self.ports)))
@property
def human_readable_state(self):
self.inspect_if_not_inspected()
if self.dictionary['State']['Running']:
if self.dictionary['State']['Ghost']:
return 'Ghost'
else:
return 'Up'
if self.is_running:
return 'Ghost' if self.get('State.Ghost') else 'Up'
else:
return 'Exit %s' % self.dictionary['State']['ExitCode']
return 'Exit %s' % self.get('State.ExitCode')
@property
def human_readable_command(self):
self.inspect_if_not_inspected()
return ' '.join(self.dictionary['Config']['Cmd'])
entrypoint = self.get('Config.Entrypoint') or []
cmd = self.get('Config.Cmd') or []
return ' '.join(entrypoint + cmd)
@property
def environment(self):
self.inspect_if_not_inspected()
out = {}
for var in self.dictionary.get('Config', {}).get('Env', []):
k, v = var.split('=', 1)
out[k] = v
return out
return dict(var.split("=", 1) for var in self.get('Config.Env') or [])
@property
def is_running(self):
return self.get('State.Running')
def get(self, key):
"""Return a value from the container or None if the value is not set.
:param key: a string using dotted notation for nested dictionary
lookups
"""
self.inspect_if_not_inspected()
return self.dictionary['State']['Running']
def get_value(dictionary, key):
return (dictionary or {}).get(key)
return reduce(get_value, key.split('.'), self.dictionary)
def get_local_port(self, port, protocol='tcp'):
port = self.ports.get("%s/%s" % (port, protocol))
return "{HostIp}:{HostPort}".format(**port[0]) if port else None
def start(self, **options):
log.info("Starting %s..." % self.name)
return self.client.start(self.id, **options)
def stop(self, **options):
log.info("Stopping %s..." % self.name)
return self.client.stop(self.id, **options)
def kill(self):
log.info("Killing %s..." % self.name)
return self.client.kill(self.id)
def remove(self):
log.info("Removing %s..." % self.name)
return self.client.remove_container(self.id)
def restart(self):
return self.client.restart(self.id)
def remove(self, **options):
return self.client.remove_container(self.id, **options)
def inspect_if_not_inspected(self):
if not self.has_been_inspected:
@@ -116,6 +145,7 @@ class Container(object):
def inspect(self):
self.dictionary = self.client.inspect_container(self.id)
self.has_been_inspected = True
return self.dictionary
def links(self):
@@ -127,6 +157,9 @@ class Container(object):
links.append(bits[2])
return links
def attach(self, *args, **kwargs):
return self.client.attach(self.id, *args, **kwargs)
def attach_socket(self, **kwargs):
return self.client.attach_socket(self.id, **kwargs)

0
fig/packages/__init__.py Normal file
View File

View File

@@ -0,0 +1,27 @@
# dockerpty.
#
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .pty import PseudoTerminal
def start(client, container):
"""
Present the PTY of the container inside the current process.
This is just a wrapper for PseudoTerminal(client, container).start()
"""
PseudoTerminal(client, container).start()

View File

@@ -0,0 +1,294 @@
# dockerpty: io.py
#
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import fcntl
import errno
import struct
import select as builtin_select
def set_blocking(fd, blocking=True):
"""
Set the given file-descriptor blocking or non-blocking.
Returns the original blocking status.
"""
old_flag = fcntl.fcntl(fd, fcntl.F_GETFL)
if blocking:
new_flag = old_flag &~ os.O_NONBLOCK
else:
new_flag = old_flag | os.O_NONBLOCK
fcntl.fcntl(fd, fcntl.F_SETFL, new_flag)
return not bool(old_flag & os.O_NONBLOCK)
def select(read_streams, timeout=0):
"""
Select the streams from `read_streams` that are ready for reading.
Uses `select.select()` internally but returns a flat list of streams.
"""
write_streams = []
exception_streams = []
try:
return builtin_select.select(
read_streams,
write_streams,
exception_streams,
timeout,
)[0]
except builtin_select.error as e:
# POSIX signals interrupt select()
if e[0] == errno.EINTR:
return []
else:
raise e
class Stream(object):
"""
Generic Stream class.
This is a file-like abstraction on top of os.read() and os.write(), which
add consistency to the reading of sockets and files alike.
"""
"""
Recoverable IO/OS Errors.
"""
ERRNO_RECOVERABLE = [
errno.EINTR,
errno.EDEADLK,
errno.EWOULDBLOCK,
]
def __init__(self, fd):
"""
Initialize the Stream for the file descriptor `fd`.
The `fd` object must have a `fileno()` method.
"""
self.fd = fd
def fileno(self):
"""
Return the fileno() of the file descriptor.
"""
return self.fd.fileno()
def set_blocking(self, value):
if hasattr(self.fd, 'setblocking'):
self.fd.setblocking(value)
return True
else:
return set_blocking(self.fd, value)
def read(self, n=4096):
"""
Return `n` bytes of data from the Stream, or None at end of stream.
"""
try:
if hasattr(self.fd, 'recv'):
return self.fd.recv(n)
return os.read(self.fd.fileno(), n)
except EnvironmentError as e:
if e.errno not in Stream.ERRNO_RECOVERABLE:
raise e
def write(self, data):
"""
Write `data` to the Stream.
"""
if not data:
return None
while True:
try:
if hasattr(self.fd, 'send'):
self.fd.send(data)
return len(data)
os.write(self.fd.fileno(), data)
return len(data)
except EnvironmentError as e:
if e.errno not in Stream.ERRNO_RECOVERABLE:
raise e
def __repr__(self):
return "{cls}({fd})".format(cls=type(self).__name__, fd=self.fd)
class Demuxer(object):
"""
Wraps a multiplexed Stream to read in data demultiplexed.
Docker multiplexes streams together when there is no PTY attached, by
sending an 8-byte header, followed by a chunk of data.
The first 4 bytes of the header denote the stream from which the data came
(i.e. 0x01 = stdout, 0x02 = stderr). Only the first byte of these initial 4
bytes is used.
The next 4 bytes indicate the length of the following chunk of data as an
integer in big endian format. This much data must be consumed before the
next 8-byte header is read.
"""
def __init__(self, stream):
"""
Initialize a new Demuxer reading from `stream`.
"""
self.stream = stream
self.remain = 0
def fileno(self):
"""
Returns the fileno() of the underlying Stream.
This is useful for select() to work.
"""
return self.stream.fileno()
def set_blocking(self, value):
return self.stream.set_blocking(value)
def read(self, n=4096):
"""
Read up to `n` bytes of data from the Stream, after demuxing.
Less than `n` bytes of data may be returned depending on the available
payload, but the number of bytes returned will never exceed `n`.
Because demuxing involves scanning 8-byte headers, the actual amount of
data read from the underlying stream may be greater than `n`.
"""
size = self._next_packet_size(n)
if size <= 0:
return
else:
return self.stream.read(size)
def write(self, data):
"""
Delegates the the underlying Stream.
"""
return self.stream.write(data)
def _next_packet_size(self, n=0):
size = 0
if self.remain > 0:
size = min(n, self.remain)
self.remain -= size
else:
data = self.stream.read(8)
if data is None:
return 0
if len(data) == 8:
__, actual = struct.unpack('>BxxxL', data)
size = min(n, actual)
self.remain = actual - size
return size
def __repr__(self):
return "{cls}({stream})".format(cls=type(self).__name__,
stream=self.stream)
class Pump(object):
"""
Stream pump class.
A Pump wraps two Streams, reading from one and and writing its data into
the other, much like a pipe but manually managed.
This abstraction is used to facilitate piping data between the file
descriptors associated with the tty and those associated with a container's
allocated pty.
Pumps are selectable based on the 'read' end of the pipe.
"""
def __init__(self, from_stream, to_stream):
"""
Initialize a Pump with a Stream to read from and another to write to.
"""
self.from_stream = from_stream
self.to_stream = to_stream
def fileno(self):
"""
Returns the `fileno()` of the reader end of the Pump.
This is useful to allow Pumps to function with `select()`.
"""
return self.from_stream.fileno()
def set_blocking(self, value):
return self.from_stream.set_blocking(value)
def flush(self, n=4096):
"""
Flush `n` bytes of data from the reader Stream to the writer Stream.
Returns the number of bytes that were actually flushed. A return value
of zero is not an error.
If EOF has been reached, `None` is returned.
"""
try:
return self.to_stream.write(self.from_stream.read(n))
except OSError as e:
if e.errno != errno.EPIPE:
raise e
def __repr__(self):
return "{cls}(from={from_stream}, to={to_stream})".format(
cls=type(self).__name__,
from_stream=self.from_stream,
to_stream=self.to_stream)

View File

@@ -0,0 +1,235 @@
# dockerpty: pty.py
#
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import signal
from ssl import SSLError
from . import io
from . import tty
class WINCHHandler(object):
"""
WINCH Signal handler to keep the PTY correctly sized.
"""
def __init__(self, pty):
"""
Initialize a new WINCH handler for the given PTY.
Initializing a handler has no immediate side-effects. The `start()`
method must be invoked for the signals to be trapped.
"""
self.pty = pty
self.original_handler = None
def __enter__(self):
"""
Invoked on entering a `with` block.
"""
self.start()
return self
def __exit__(self, *_):
"""
Invoked on exiting a `with` block.
"""
self.stop()
def start(self):
"""
Start trapping WINCH signals and resizing the PTY.
This method saves the previous WINCH handler so it can be restored on
`stop()`.
"""
def handle(signum, frame):
if signum == signal.SIGWINCH:
self.pty.resize()
self.original_handler = signal.signal(signal.SIGWINCH, handle)
def stop(self):
"""
Stop trapping WINCH signals and restore the previous WINCH handler.
"""
if self.original_handler is not None:
signal.signal(signal.SIGWINCH, self.original_handler)
class PseudoTerminal(object):
"""
Wraps the pseudo-TTY (PTY) allocated to a docker container.
The PTY is managed via the current process' TTY until it is closed.
Example:
import docker
from dockerpty import PseudoTerminal
client = docker.Client()
container = client.create_container(
image='busybox:latest',
stdin_open=True,
tty=True,
command='/bin/sh',
)
# hijacks the current tty until the pty is closed
PseudoTerminal(client, container).start()
Care is taken to ensure all file descriptors are restored on exit. For
example, you can attach to a running container from within a Python REPL
and when the container exits, the user will be returned to the Python REPL
without adverse effects.
"""
def __init__(self, client, container):
"""
Initialize the PTY using the docker.Client instance and container dict.
"""
self.client = client
self.container = container
self.raw = None
def start(self, **kwargs):
"""
Present the PTY of the container inside the current process.
This will take over the current process' TTY until the container's PTY
is closed.
"""
pty_stdin, pty_stdout, pty_stderr = self.sockets()
mappings = [
(io.Stream(sys.stdin), pty_stdin),
(pty_stdout, io.Stream(sys.stdout)),
(pty_stderr, io.Stream(sys.stderr)),
]
pumps = [io.Pump(a, b) for (a, b) in mappings if a and b]
if not self.container_info()['State']['Running']:
self.client.start(self.container, **kwargs)
flags = [p.set_blocking(False) for p in pumps]
try:
with WINCHHandler(self):
self._hijack_tty(pumps)
finally:
if flags:
for (pump, flag) in zip(pumps, flags):
io.set_blocking(pump, flag)
def israw(self):
"""
Returns True if the PTY should operate in raw mode.
If the container was not started with tty=True, this will return False.
"""
if self.raw is None:
info = self.container_info()
self.raw = sys.stdout.isatty() and info['Config']['Tty']
return self.raw
def sockets(self):
"""
Returns a tuple of sockets connected to the pty (stdin,stdout,stderr).
If any of the sockets are not attached in the container, `None` is
returned in the tuple.
"""
info = self.container_info()
def attach_socket(key):
if info['Config']['Attach{0}'.format(key.capitalize())]:
socket = self.client.attach_socket(
self.container,
{key: 1, 'stream': 1, 'logs': 1},
)
stream = io.Stream(socket)
if info['Config']['Tty']:
return stream
else:
return io.Demuxer(stream)
else:
return None
return map(attach_socket, ('stdin', 'stdout', 'stderr'))
def resize(self, size=None):
"""
Resize the container's PTY.
If `size` is not None, it must be a tuple of (height,width), otherwise
it will be determined by the size of the current TTY.
"""
if not self.israw():
return
size = size or tty.size(sys.stdout)
if size is not None:
rows, cols = size
try:
self.client.resize(self.container, height=rows, width=cols)
except IOError: # Container already exited
pass
def container_info(self):
"""
Thin wrapper around client.inspect_container().
"""
return self.client.inspect_container(self.container)
def _hijack_tty(self, pumps):
with tty.Terminal(sys.stdin, raw=self.israw()):
self.resize()
while True:
_ready = io.select(pumps, timeout=60)
try:
if all([p.flush() is None for p in pumps]):
break
except SSLError as e:
if 'The operation did not complete' not in e.strerror:
raise e

View File

@@ -0,0 +1,130 @@
# dockerpty: tty.py
#
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import
import os
import termios
import tty
import fcntl
import struct
def size(fd):
"""
Return a tuple (rows,cols) representing the size of the TTY `fd`.
The provided file descriptor should be the stdout stream of the TTY.
If the TTY size cannot be determined, returns None.
"""
if not os.isatty(fd.fileno()):
return None
try:
dims = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'hhhh'))
except:
try:
dims = (os.environ['LINES'], os.environ['COLUMNS'])
except:
return None
return dims
class Terminal(object):
"""
Terminal provides wrapper functionality to temporarily make the tty raw.
This is useful when streaming data from a pseudo-terminal into the tty.
Example:
with Terminal(sys.stdin, raw=True):
do_things_in_raw_mode()
"""
def __init__(self, fd, raw=True):
"""
Initialize a terminal for the tty with stdin attached to `fd`.
Initializing the Terminal has no immediate side effects. The `start()`
method must be invoked, or `with raw_terminal:` used before the
terminal is affected.
"""
self.fd = fd
self.raw = raw
self.original_attributes = None
def __enter__(self):
"""
Invoked when a `with` block is first entered.
"""
self.start()
return self
def __exit__(self, *_):
"""
Invoked when a `with` block is finished.
"""
self.stop()
def israw(self):
"""
Returns True if the TTY should operate in raw mode.
"""
return self.raw
def start(self):
"""
Saves the current terminal attributes and makes the tty raw.
This method returns None immediately.
"""
if os.isatty(self.fd.fileno()) and self.israw():
self.original_attributes = termios.tcgetattr(self.fd)
tty.setraw(self.fd)
def stop(self):
"""
Restores the terminal attributes back to before setting raw mode.
If the raw terminal was not started, does nothing.
"""
if self.original_attributes is not None:
termios.tcsetattr(
self.fd,
termios.TCSADRAIN,
self.original_attributes,
)
def __repr__(self):
return "{cls}({fd}, raw={raw})".format(
cls=type(self).__name__,
fd=self.fd,
raw=self.raw)

83
fig/progress_stream.py Normal file
View File

@@ -0,0 +1,83 @@
import json
import os
import codecs
class StreamOutputError(Exception):
pass
def stream_output(output, stream):
is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno())
stream = codecs.getwriter('utf-8')(stream)
all_events = []
lines = {}
diff = 0
for chunk in output:
event = json.loads(chunk)
all_events.append(event)
if 'progress' in event or 'progressDetail' in event:
image_id = event['id']
if image_id in lines:
diff = len(lines) - lines[image_id]
else:
lines[image_id] = len(lines)
stream.write("\n")
diff = 0
if is_terminal:
# move cursor up `diff` rows
stream.write("%c[%dA" % (27, diff))
print_output_event(event, stream, is_terminal)
if 'id' in event and is_terminal:
# move cursor back down
stream.write("%c[%dB" % (27, diff))
stream.flush()
return all_events
def print_output_event(event, stream, is_terminal):
if 'errorDetail' in event:
raise StreamOutputError(event['errorDetail']['message'])
terminator = ''
if is_terminal and 'stream' not in event:
# erase current line
stream.write("%c[2K\r" % 27)
terminator = "\r"
pass
elif 'progressDetail' in event:
return
if 'time' in event:
stream.write("[%s] " % event['time'])
if 'id' in event:
stream.write("%s: " % event['id'])
if 'from' in event:
stream.write("(from %s) " % event['from'])
status = event.get('status', '')
if 'progress' in event:
stream.write("%s %s%s" % (status, event['progress'], terminator))
elif 'progressDetail' in event:
detail = event['progressDetail']
if 'current' in detail:
percentage = float(detail['current']) / float(detail['total']) * 100
stream.write('%s (%.1f%%)%s' % (status, percentage, terminator))
else:
stream.write('%s%s' % (status, terminator))
elif 'stream' in event:
stream.write("%s%s" % (event['stream'], terminator))
else:
stream.write("%s%s\n" % (status, terminator))

View File

@@ -1,16 +1,44 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from .service import Service
from .container import Container
from docker.errors import APIError
log = logging.getLogger(__name__)
def sort_service_dicts(services):
# Sort in dependency order
def cmp(x, y):
x_deps_y = y['name'] in x.get('links', [])
y_deps_x = x['name'] in y.get('links', [])
if x_deps_y and not y_deps_x:
return 1
elif y_deps_x and not x_deps_y:
return -1
return 0
return sorted(services, cmp=cmp)
# Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:]
temporary_marked = set()
sorted_services = []
get_service_names = lambda links: [link.split(':')[0] for link in links]
def visit(n):
if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])):
raise DependencyError('A service can not link to itself: %s' % n['name'])
if n['name'] in n.get('volumes_from', []):
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
else:
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
if n in unmarked:
temporary_marked.add(n['name'])
dependents = [m for m in services if (n['name'] in get_service_names(m.get('links', []))) or (n['name'] in m.get('volumes_from', []))]
for m in dependents:
visit(m)
temporary_marked.remove(n['name'])
unmarked.remove(n)
sorted_services.insert(0, n)
while unmarked:
visit(unmarked[-1])
return sorted_services
class Project(object):
"""
@@ -28,19 +56,18 @@ class Project(object):
"""
project = cls(name, [], client)
for service_dict in sort_service_dicts(service_dicts):
# Reference links by object
links = []
if 'links' in service_dict:
for service_name in service_dict.get('links', []):
links.append(project.get_service(service_name))
del service_dict['links']
project.services.append(Service(client=client, project=name, links=links, **service_dict))
links = project.get_links(service_dict)
volumes_from = project.get_volumes_from(service_dict)
project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict))
return project
@classmethod
def from_config(cls, name, config, client):
dicts = []
for service_name, service in config.items():
for service_name, service in list(config.items()):
if not isinstance(service, dict):
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.')
service['name'] = service_name
dicts.append(service)
return cls.from_dicts(name, dicts, client)
@@ -56,61 +83,130 @@ class Project(object):
raise NoSuchService(name)
def get_services(self, service_names=None):
def get_services(self, service_names=None, include_links=False):
"""
Returns a list of this project's services filtered
by the provided list of names, or all services if
service_names is None or [].
by the provided list of names, or all services if service_names is None
or [].
Preserves the original order of self.services.
If include_links is specified, returns a list including the links for
service_names, in order of dependency.
Raises NoSuchService if any of the named services
do not exist.
Preserves the original order of self.services where possible,
reordering as needed to resolve links.
Raises NoSuchService if any of the named services do not exist.
"""
if service_names is None or len(service_names) == 0:
return self.services
return self.get_services(
service_names=[s.name for s in self.services],
include_links=include_links
)
else:
unsorted = [self.get_service(name) for name in service_names]
return [s for s in self.services if s in unsorted]
services = [s for s in self.services if s in unsorted]
def create_containers(self, service_names=None):
"""
Returns a list of (service, container) tuples,
one for each service with no running containers.
"""
containers = []
for service in self.get_services(service_names):
if len(service.containers()) == 0:
containers.append((service, service.create_container()))
return containers
if include_links:
services = reduce(self._inject_links, services, [])
def kill_and_remove(self, tuples):
for (service, container) in tuples:
container.kill()
container.remove()
uniques = []
[uniques.append(s) for s in services if s not in uniques]
return uniques
def get_links(self, service_dict):
links = []
if 'links' in service_dict:
for link in service_dict.get('links', []):
if ':' in link:
service_name, link_name = link.split(':', 1)
else:
service_name, link_name = link, None
try:
links.append((self.get_service(service_name), link_name))
except NoSuchService:
raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
del service_dict['links']
return links
def get_volumes_from(self, service_dict):
volumes_from = []
if 'volumes_from' in service_dict:
for volume_name in service_dict.get('volumes_from', []):
try:
service = self.get_service(volume_name)
volumes_from.append(service)
except NoSuchService:
try:
container = Container.from_id(self.client, volume_name)
volumes_from.append(container)
except APIError:
raise ConfigurationError('Service "%s" mounts volumes from "%s", which is not the name of a service or container.' % (service_dict['name'], volume_name))
del service_dict['volumes_from']
return volumes_from
def start(self, service_names=None, **options):
for service in self.get_services(service_names):
service.start(**options)
def stop(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.stop(**options)
def kill(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.kill(**options)
def restart(self, service_names=None, **options):
for service in self.get_services(service_names):
service.restart(**options)
def build(self, service_names=None, no_cache=False):
for service in self.get_services(service_names):
if service.can_be_built():
service.build(no_cache)
else:
log.info('%s uses an image, skipping' % service.name)
def up(self, service_names=None, start_links=True, recreate=True):
running_containers = []
for service in self.get_services(service_names, include_links=start_links):
if recreate:
for (_, container) in service.recreate_containers():
running_containers.append(container)
else:
for container in service.start_or_create_containers():
running_containers.append(container)
return running_containers
def pull(self, service_names=None, insecure_registry=False):
for service in self.get_services(service_names, include_links=True):
service.pull(insecure_registry=insecure_registry)
def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
service.remove_stopped(**options)
def containers(self, service_names=None, *args, **kwargs):
l = []
for service in self.get_services(service_names):
for container in service.containers(*args, **kwargs):
l.append(container)
return l
def containers(self, service_names=None, stopped=False, one_off=False):
return [Container.from_ps(self.client, container)
for container in self.client.containers(all=stopped)
for service in self.get_services(service_names)
if service.has_container(container, one_off=one_off)]
def _inject_links(self, acc, service):
linked_names = service.get_linked_names()
if len(linked_names) > 0:
linked_services = self.get_services(
service_names=linked_names,
include_links=True
)
else:
linked_services = []
linked_services.append(service)
return acc + linked_services
class NoSuchService(Exception):
@@ -120,3 +216,15 @@ class NoSuchService(Exception):
def __str__(self):
return self.msg
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class DependencyError(ConfigurationError):
pass

View File

@@ -1,59 +1,171 @@
from docker.client import APIError
from __future__ import unicode_literals
from __future__ import absolute_import
from collections import namedtuple
import logging
import re
import os
from operator import attrgetter
import sys
from docker.errors import APIError
from .container import Container
from .progress_stream import stream_output, StreamOutputError
log = logging.getLogger(__name__)
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir']
DOCKER_CONFIG_HINTS = {
'link' : 'links',
'port' : 'ports',
'privilege' : 'privileged',
'priviliged': 'privileged',
'privilige' : 'privileged',
'volume' : 'volumes',
'workdir' : 'working_dir',
}
VALID_NAME_CHARS = '[a-zA-Z0-9]'
class BuildError(Exception):
def __init__(self, service, reason):
self.service = service
self.reason = reason
class CannotBeScaledError(Exception):
pass
class ConfigError(ValueError):
pass
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
ServiceName = namedtuple('ServiceName', 'project service number')
class Service(object):
def __init__(self, name, client=None, project='default', links=[], **options):
if not re.match('^[a-zA-Z0-9]+$', name):
raise ValueError('Invalid name: %s' % name)
if not re.match('^[a-zA-Z0-9]+$', project):
raise ValueError('Invalid project: %s' % project)
def __init__(self, name, client=None, project='default', links=None, volumes_from=None, **options):
if not re.match('^%s+$' % VALID_NAME_CHARS, name):
raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
if not re.match('^%s+$' % VALID_NAME_CHARS, project):
raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS))
if 'image' in options and 'build' in options:
raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose']
for k in options:
if k not in supported_options:
msg = "Unsupported config option for %s service: '%s'" % (name, k)
if k in DOCKER_CONFIG_HINTS:
msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k]
raise ConfigError(msg)
self.name = name
self.client = client
self.project = project
self.links = links or []
self.volumes_from = volumes_from or []
self.options = options
def containers(self, stopped=False, one_off=False):
l = []
for container in self.client.containers(all=stopped):
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
return [Container.from_ps(self.client, container)
for container in self.client.containers(all=stopped)
if self.has_container(container, one_off=one_off)]
def has_container(self, container, one_off=False):
"""Return True if `container` was created to fulfill this service."""
name = get_container_name(container)
if not name or not is_valid_name(name, one_off):
return False
project, name, _number = parse_name(name)
return project == self.project and name == self.name
def get_container(self, number=1):
"""Return a :class:`fig.container.Container` for this service. The
container must be active, and match `number`.
"""
for container in self.client.containers():
if not self.has_container(container):
continue
project, name, number = parse_name(name)
if project == self.project and name == self.name:
l.append(Container.from_ps(self.client, container))
return l
_, _, container_number = parse_name(get_container_name(container))
if container_number == number:
return Container.from_ps(self.client, container)
raise ValueError("No container found for %s_%s" % (self.name, number))
def start(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
self.start_container(c, **options)
self.start_container_if_stopped(c, **options)
def stop(self, **options):
for c in self.containers():
log.info("Stopping %s..." % c.name)
c.stop(**options)
def kill(self, **options):
for c in self.containers():
log.info("Killing %s..." % c.name)
c.kill(**options)
def restart(self, **options):
for c in self.containers():
log.info("Restarting %s..." % c.name)
c.restart(**options)
def scale(self, desired_num):
"""
Adjusts the number of containers to the specified number and ensures they are running.
- creates containers until there are at least `desired_num`
- stops containers until there are at most `desired_num` running
- starts containers until there are at least `desired_num` running
- removes all stopped containers
"""
if not self.can_be_scaled():
raise CannotBeScaledError()
# Create enough containers
containers = self.containers(stopped=True)
while len(containers) < desired_num:
containers.append(self.create_container())
running_containers = []
stopped_containers = []
for c in containers:
if c.is_running:
running_containers.append(c)
else:
stopped_containers.append(c)
running_containers.sort(key=lambda c: c.number)
stopped_containers.sort(key=lambda c: c.number)
# Stop containers
while len(running_containers) > desired_num:
c = running_containers.pop()
log.info("Stopping %s..." % c.name)
c.stop(timeout=1)
stopped_containers.append(c)
# Start containers
while len(running_containers) < desired_num:
c = stopped_containers.pop(0)
log.info("Starting %s..." % c.name)
self.start_container(c)
running_containers.append(c)
self.remove_stopped()
def remove_stopped(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
log.info("Removing %s..." % c.name)
c.remove(**options)
def create_container(self, one_off=False, **override_options):
@@ -61,118 +173,264 @@ class Service(object):
Create a container for this service. If the image doesn't exist, attempt to pull
it.
"""
container_options = self._get_container_options(override_options, one_off=one_off)
container_options = self._get_container_create_options(override_options, one_off=one_off)
try:
return Container.create(self.client, **container_options)
except APIError, e:
if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation:
except APIError as e:
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
log.info('Pulling image %s...' % container_options['image'])
self.client.pull(container_options['image'])
output = self.client.pull(container_options['image'], stream=True)
stream_output(output, sys.stdout)
return Container.create(self.client, **container_options)
raise
def start_container(self, container=None, **override_options):
if container is None:
def recreate_containers(self, **override_options):
"""
If a container for this service doesn't exist, create and start one. If there are
any, stop them, create+start new ones, and remove the old containers.
"""
containers = self.containers(stopped=True)
if not containers:
log.info("Creating %s..." % self._next_container_name(containers))
container = self.create_container(**override_options)
self.start_container(container)
return [(None, container)]
else:
tuples = []
options = self.options.copy()
options.update(override_options)
for c in containers:
log.info("Recreating %s..." % c.name)
tuples.append(self.recreate_container(c, **override_options))
port_bindings = {}
return tuples
if options.get('ports', None) is not None:
for port in options['ports']:
port = unicode(port)
if ':' in port:
internal_port, external_port = port.split(':', 1)
port_bindings[int(internal_port)] = int(external_port)
else:
port_bindings[int(port)] = None
def recreate_container(self, container, **override_options):
"""Recreate a container. An intermediate container is created so that
the new container has the same name, while still supporting
`volumes-from` the original container.
"""
try:
container.stop()
except APIError as e:
if (e.response.status_code == 500
and e.explanation
and 'no such process' in str(e.explanation)):
pass
else:
raise
volume_bindings = {}
intermediate_container = Container.create(
self.client,
image=container.image,
entrypoint=['/bin/echo'],
command=[],
)
intermediate_container.start(volumes_from=container.id)
intermediate_container.wait()
container.remove()
if options.get('volumes', None) is not None:
for volume in options['volumes']:
external_dir, internal_dir = volume.split(':')
volume_bindings[os.path.abspath(external_dir)] = internal_dir
options = dict(override_options)
new_container = self.create_container(**options)
self.start_container(new_container, intermediate_container=intermediate_container)
intermediate_container.remove()
return (intermediate_container, new_container)
def start_container_if_stopped(self, container, **options):
if container.is_running:
return container
else:
log.info("Starting %s..." % container.name)
return self.start_container(container, **options)
def start_container(self, container=None, intermediate_container=None, **override_options):
container = container or self.create_container(**override_options)
options = dict(self.options, **override_options)
ports = dict(split_port(port) for port in options.get('ports') or [])
volume_bindings = dict(
build_volume_binding(parse_volume_spec(volume))
for volume in options.get('volumes') or []
if ':' in volume)
privileged = options.get('privileged', False)
net = options.get('net', 'bridge')
dns = options.get('dns', None)
container.start(
links=self._get_links(),
port_bindings=port_bindings,
links=self._get_links(link_to_self=options.get('one_off', False)),
port_bindings=ports,
binds=volume_bindings,
volumes_from=self._get_volumes_from(intermediate_container),
privileged=privileged,
network_mode=net,
dns=dns,
)
return container
def next_container_name(self, one_off=False):
def start_or_create_containers(self):
containers = self.containers(stopped=True)
if not containers:
log.info("Creating %s..." % self._next_container_name(containers))
new_container = self.create_container()
return [self.start_container(new_container)]
else:
return [self.start_container_if_stopped(c) for c in containers]
def get_linked_names(self):
return [s.name for (s, _) in self.links]
def _next_container_name(self, all_containers, one_off=False):
bits = [self.project, self.name]
if one_off:
bits.append('run')
return '_'.join(bits + [unicode(self.next_container_number(one_off=one_off))])
return '_'.join(bits + [str(self._next_container_number(all_containers))])
def next_container_number(self, one_off=False):
numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)]
def _next_container_number(self, all_containers):
numbers = [parse_name(c.name).number for c in all_containers]
return 1 if not numbers else max(numbers) + 1
if len(numbers) == 0:
return 1
else:
return max(numbers) + 1
def _get_links(self):
links = {}
for service in self.links:
def _get_links(self, link_to_self):
links = []
for service, link_name in self.links:
for container in service.containers():
links[container.name] = container.name
links.append((container.name, link_name or service.name))
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
if link_to_self:
for container in self.containers():
links.append((container.name, self.name))
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
def _get_volumes_from(self, intermediate_container=None):
volumes_from = []
for volume_source in self.volumes_from:
if isinstance(volume_source, Service):
containers = volume_source.containers(stopped=True)
if not containers:
volumes_from.append(volume_source.create_container().id)
else:
volumes_from.extend(map(attrgetter('id'), containers))
elif isinstance(volume_source, Container):
volumes_from.append(volume_source.id)
if intermediate_container:
volumes_from.append(intermediate_container.id)
return volumes_from
def _get_container_create_options(self, override_options, one_off=False):
container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options)
container_options.update(override_options)
container_options['name'] = self.next_container_name(one_off)
container_options['name'] = self._next_container_name(
self.containers(stopped=True, one_off=one_off),
one_off)
if 'ports' in container_options:
container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']]
# If a qualified hostname was given, split it into an
# unqualified hostname and a domainname unless domainname
# was also given explicitly. This matches the behavior of
# the official Docker CLI in that scenario.
if ('hostname' in container_options
and 'domainname' not in container_options
and '.' in container_options['hostname']):
parts = container_options['hostname'].partition('.')
container_options['hostname'] = parts[0]
container_options['domainname'] = parts[2]
if 'ports' in container_options or 'expose' in self.options:
ports = []
all_ports = container_options.get('ports', []) + self.options.get('expose', [])
for port in all_ports:
port = str(port)
if ':' in port:
port = port.split(':')[-1]
if '/' in port:
port = tuple(port.split('/'))
ports.append(port)
container_options['ports'] = ports
if 'volumes' in container_options:
container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes'])
container_options['volumes'] = dict(
(parse_volume_spec(v).internal, {})
for v in container_options['volumes'])
if 'build' in self.options:
if 'environment' in container_options:
if isinstance(container_options['environment'], list):
container_options['environment'] = dict(split_env(e) for e in container_options['environment'])
container_options['environment'] = dict(resolve_env(k, v) for k, v in container_options['environment'].iteritems())
if self.can_be_built():
if len(self.client.images(name=self._build_tag_name())) == 0:
self.build()
container_options['image'] = self._build_tag_name()
# Delete options which are only used when starting
for key in ['privileged', 'net', 'dns']:
if key in container_options:
del container_options[key]
return container_options
def build(self):
def build(self, no_cache=False):
log.info('Building %s...' % self.name)
build_output = self.client.build(
self.options['build'],
tag=self._build_tag_name(),
stream=True
stream=True,
rm=True,
nocache=no_cache,
)
try:
all_events = stream_output(build_output, sys.stdout)
except StreamOutputError, e:
raise BuildError(self, unicode(e))
image_id = None
for line in build_output:
if line:
match = re.search(r'Successfully built ([0-9a-f]+)', line)
for event in all_events:
if 'stream' in event:
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
if match:
image_id = match.group(1)
sys.stdout.write(line)
if image_id is None:
raise BuildError()
raise BuildError(self)
return image_id
def can_be_built(self):
return 'build' in self.options
def _build_tag_name(self):
"""
The tag to give to images built for this service.
"""
return '%s_%s' % (self.project, self.name)
def can_be_scaled(self):
for port in self.options.get('ports', []):
if ':' in str(port):
return False
return True
def pull(self, insecure_registry=False):
if 'image' in self.options:
log.info('Pulling %s (%s)...' % (self.name, self.options.get('image')))
self.client.pull(
self.options.get('image'),
insecure_registry=insecure_registry
)
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
@@ -187,10 +445,10 @@ def is_valid_name(name, one_off=False):
return match.group(3) is None
def parse_name(name, one_off=False):
def parse_name(name):
match = NAME_RE.match(name)
(project, service_name, _, suffix) = match.groups()
return (project, service_name, int(suffix))
return ServiceName(project, service_name, int(suffix))
def get_container_name(container):
@@ -203,3 +461,62 @@ def get_container_name(container):
for name in container['Names']:
if len(name.split('/')) == 2:
return name[1:]
def parse_volume_spec(volume_config):
parts = volume_config.split(':')
if len(parts) > 3:
raise ConfigError("Volume %s has incorrect format, should be "
"external:internal[:mode]" % volume_config)
if len(parts) == 1:
return VolumeSpec(None, parts[0], 'rw')
if len(parts) == 2:
parts.append('rw')
external, internal, mode = parts
if mode not in ('rw', 'ro'):
raise ConfigError("Volume %s has invalid mode (%s), should be "
"one of: rw, ro." % (volume_config, mode))
return VolumeSpec(external, internal, mode)
def build_volume_binding(volume_spec):
internal = {'bind': volume_spec.internal, 'ro': volume_spec.mode == 'ro'}
external = os.path.expanduser(volume_spec.external)
return os.path.abspath(os.path.expandvars(external)), internal
def split_port(port):
parts = str(port).split(':')
if not 1 <= len(parts) <= 3:
raise ConfigError('Invalid port "%s", should be '
'[[remote_ip:]remote_port:]port[/protocol]' % port)
if len(parts) == 1:
internal_port, = parts
return internal_port, None
if len(parts) == 2:
external_port, internal_port = parts
return internal_port, external_port
external_ip, external_port, internal_port = parts
return internal_port, (external_ip, external_port or None)
def split_env(env):
if '=' in env:
return env.split('=', 1)
else:
return env, None
def resolve_env(key, val):
if val is not None:
return key, val
elif key in os.environ:
return key, os.environ[key]
else:
return key, ''

View File

@@ -1 +1,5 @@
mock >= 1.0.1
nose
git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller
unittest2
flake8

View File

@@ -1,4 +1,7 @@
docker-py==0.2.3
docopt==0.6.1
PyYAML==3.10
docker-py==0.5.3
docopt==0.6.1
requests==2.2.1
six==1.7.3
texttable==0.8.1
websocket-client==0.11.0

33
script/.validate Normal file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
if [ -z "$VALIDATE_UPSTREAM" ]; then
# this is kind of an expensive check, so let's not do this twice if we
# are running more than one validate bundlescript
VALIDATE_REPO='https://github.com/docker/fig.git'
VALIDATE_BRANCH='master'
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
fi
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
validate_diff() {
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
git diff "$VALIDATE_COMMIT_DIFF" "$@"
fi
}
validate_log() {
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
git log "$VALIDATE_COMMIT_LOG" "$@"
fi
}
fi

5
script/build-docs Executable file
View File

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

8
script/build-linux Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -ex
mkdir -p `pwd`/dist
chmod 777 `pwd`/dist
docker build -t fig .
docker run -u user -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig
mv dist/fig dist/fig-Linux-x86_64
docker run -u user -v `pwd`/dist:/code/dist fig dist/fig-Linux-x86_64 --version

10
script/build-osx Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
set -ex
rm -rf venv
virtualenv venv
venv/bin/pip install -r requirements.txt
venv/bin/pip install -r requirements-dev.txt
venv/bin/pip install .
venv/bin/pyinstaller -F bin/fig
mv dist/fig dist/fig-Darwin-x86_64
dist/fig-Darwin-x86_64 --version

View File

@@ -1,3 +1,3 @@
#!/bin/sh
find . -type f -name '*.pyc' -delete
rm -rf docs/_site build dist fig.egg-info

29
script/deploy-docs Executable file
View File

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

2
script/open-docs Executable file
View File

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

View File

@@ -1,14 +0,0 @@
#!/bin/bash
set -xe
if [ -z "$1" ]; then
echo 'pass a version as first argument'
exit 1
fi
git tag $1
git push --tags
python setup.py sdist upload

View File

@@ -1,2 +1,12 @@
#!/bin/sh
nosetests
set -ex
target="tests"
if [[ -n "$@" ]]; then
target="$@"
fi
docker build -t fig .
docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 --exclude=packages fig
docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $target

56
script/validate-dco Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
source "$(dirname "$BASH_SOURCE")/.validate"
adds=$(validate_diff --numstat | awk '{ s += $1 } END { print s }')
dels=$(validate_diff --numstat | awk '{ s += $2 } END { print s }')
notDocs="$(validate_diff --numstat | awk '$3 !~ /^docs\// { print $3 }')"
: ${adds:=0}
: ${dels:=0}
# "Username may only contain alphanumeric characters or dashes and cannot begin with a dash"
githubUsernameRegex='[a-zA-Z0-9][a-zA-Z0-9-]+'
# https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work
dcoPrefix='Signed-off-by:'
dcoRegex="^(Docker-DCO-1.1-)?$dcoPrefix ([^<]+) <([^<>@]+@[^<>]+)>( \\(github: ($githubUsernameRegex)\\))?$"
check_dco() {
grep -qE "$dcoRegex"
}
if [ $adds -eq 0 -a $dels -eq 0 ]; then
echo '0 adds, 0 deletions; nothing to validate! :)'
elif [ -z "$notDocs" -a $adds -le 1 -a $dels -le 1 ]; then
echo 'Congratulations! DCO small-patch-exception material!'
else
commits=( $(validate_log --format='format:%H%n') )
badCommits=()
for commit in "${commits[@]}"; do
if [ -z "$(git log -1 --format='format:' --name-status "$commit")" ]; then
# no content (ie, Merge commit, etc)
continue
fi
if ! git log -1 --format='format:%B' "$commit" | check_dco; then
badCommits+=( "$commit" )
fi
done
if [ ${#badCommits[@]} -eq 0 ]; then
echo "Congratulations! All commits are properly signed with the DCO!"
else
{
echo "These commits do not have a proper '$dcoPrefix' marker:"
for commit in "${badCommits[@]}"; do
echo " - $commit"
done
echo
echo 'Please amend each commit to include a properly formatted DCO marker.'
echo
echo 'Visit the following URL for information about the Docker DCO:'
echo ' https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work'
echo
} >&2
false
fi
fi

View File

@@ -1,16 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
import re
import os
from __future__ import unicode_literals
from __future__ import absolute_import
from setuptools import setup, find_packages
import codecs
import os
import re
import sys
# Borrowed from
# https://github.com/jezdez/django_compressor/blob/develop/setup.py
def read(*parts):
return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()
path = os.path.join(os.path.dirname(__file__), *parts)
with codecs.open(path, encoding='utf-8') as fobj:
return fobj.read()
def find_version(*file_paths):
@@ -21,24 +23,41 @@ def find_version(*file_paths):
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
with open('requirements.txt') as f:
install_requires = f.read().splitlines()
with open('README.md') as f:
long_description = f.read()
install_requires = [
'docopt >= 0.6.1, < 0.7',
'PyYAML >= 3.10, < 4',
'requests >= 2.2.1, < 3',
'texttable >= 0.8.1, < 0.9',
'websocket-client >= 0.11.0, < 0.12',
'docker-py >= 0.5, < 0.6',
'six >= 1.3.0, < 2',
]
tests_require = [
'mock >= 1.0.1',
'nose',
'pyinstaller',
'flake8',
]
if sys.version_info < (2, 7):
tests_require.append('unittest2')
setup(
name='fig',
version=find_version("fig", "__init__.py"),
description='Punctual, lightweight development environments using Docker',
long_description=long_description,
url='https://github.com/orchardup/fig',
author='Orchard Laboratories Ltd.',
author_email='hello@orchardup.com',
packages=['fig', 'fig.cli'],
package_data={},
url='http://www.fig.sh/',
author='Docker, Inc.',
license='Apache License 2.0',
packages=find_packages(exclude=[ 'tests.*', 'tests' ]),
include_package_data=True,
test_suite='nose.collector',
install_requires=install_requires,
tests_require=tests_require,
entry_points="""
[console_scripts]
fig=fig.cli.main:main

View File

@@ -0,0 +1,7 @@
import sys
if sys.version_info >= (2,7):
import unittest
else:
import unittest2 as unittest

View File

@@ -1,36 +0,0 @@
from .testcases import DockerClientTestCase
from fig.container import Container
class ContainerTest(DockerClientTestCase):
def test_from_ps(self):
container = Container.from_ps(self.client, {
"Id":"abc",
"Image":"ubuntu:12.04",
"Command":"sleep 300",
"Created":1387384730,
"Status":"Up 8 seconds",
"Ports":None,
"SizeRw":0,
"SizeRootFs":0,
"Names":["/db_1"]
}, has_been_inspected=True)
self.assertEqual(container.dictionary, {
"ID": "abc",
"Image":"ubuntu:12.04",
"Name": "/db_1",
})
def test_environment(self):
container = Container(self.client, {
'ID': 'abc',
'Config': {
'Env': [
'FOO=BAR',
'BAZ=DOGE',
]
}
}, has_been_inspected=True)
self.assertEqual(container.environment, {
'FOO': 'BAR',
'BAZ': 'DOGE',
})

View File

@@ -0,0 +1,5 @@
implicit:
image: figtest_test
explicit:
image: figtest_test
command: [ "/bin/true" ]

View File

@@ -0,0 +1,2 @@
FROM busybox:latest
ENTRYPOINT echo "From prebuilt entrypoint"

View File

@@ -0,0 +1,2 @@
service:
build: tests/fixtures/dockerfile_with_entrypoint

View File

@@ -0,0 +1,7 @@
service:
image: busybox:latest
command: sleep 5
environment:
foo: bar
hello: world

11
tests/fixtures/links-figfile/fig.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
db:
image: busybox:latest
command: /bin/sleep 300
web:
image: busybox:latest
command: /bin/sleep 300
links:
- db:db
console:
image: busybox:latest
command: /bin/sleep 300

View File

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

View File

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

View File

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

7
tests/fixtures/ports-figfile/fig.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
simple:
image: busybox:latest
command: /bin/sleep 300
ports:
- '3000'
- '9999:3001'

View File

@@ -1,2 +1,2 @@
FROM ubuntu
FROM busybox:latest
CMD echo "success"

View File

@@ -0,0 +1,2 @@
simple:
build: tests/fixtures/simple-dockerfile

6
tests/fixtures/simple-figfile/fig.yml vendored Normal file
View File

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

View File

View File

@@ -0,0 +1,287 @@
from __future__ import absolute_import
import sys
from six import StringIO
from mock import patch
from .testcases import DockerClientTestCase
from fig.cli.main import TopLevelCommand
class CLITestCase(DockerClientTestCase):
def setUp(self):
super(CLITestCase, self).setUp()
self.old_sys_exit = sys.exit
sys.exit = lambda code=0: None
self.command = TopLevelCommand()
self.command.base_dir = 'tests/fixtures/simple-figfile'
def tearDown(self):
sys.exit = self.old_sys_exit
self.project.kill()
self.project.remove_stopped()
@property
def project(self):
return self.command.get_project(self.command.get_config_path())
@patch('sys.stdout', new_callable=StringIO)
def test_ps(self, mock_stdout):
self.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_ps_default_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['up', '-d'], None)
self.command.dispatch(['ps'], None)
output = mock_stdout.getvalue()
self.assertIn('multiplefigfiles_simple_1', output)
self.assertIn('multiplefigfiles_another_1', output)
self.assertNotIn('multiplefigfiles_yetanother_1', output)
@patch('sys.stdout', new_callable=StringIO)
def test_ps_alternate_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
output = mock_stdout.getvalue()
self.assertNotIn('multiplefigfiles_simple_1', output)
self.assertNotIn('multiplefigfiles_another_1', output)
self.assertIn('multiplefigfiles_yetanother_1', output)
@patch('fig.service.log')
def test_pull(self, mock_logging):
self.command.dispatch(['pull'], None)
mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
@patch('sys.stdout', new_callable=StringIO)
def test_build_no_cache(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/simple-dockerfile'
self.command.dispatch(['build', 'simple'], None)
mock_stdout.truncate(0)
cache_indicator = 'Using cache'
self.command.dispatch(['build', 'simple'], None)
output = mock_stdout.getvalue()
self.assertIn(cache_indicator, output)
mock_stdout.truncate(0)
self.command.dispatch(['build', '--no-cache', 'simple'], None)
output = mock_stdout.getvalue()
self.assertNotIn(cache_indicator, output)
def test_up(self):
self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
another = self.project.get_service('another')
self.assertEqual(len(service.containers()), 1)
self.assertEqual(len(another.containers()), 1)
def test_up_with_links(self):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['up', '-d', 'web'], None)
web = self.project.get_service('web')
db = self.project.get_service('db')
console = self.project.get_service('console')
self.assertEqual(len(web.containers()), 1)
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
def test_up_with_no_deps(self):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
web = self.project.get_service('web')
db = self.project.get_service('db')
console = self.project.get_service('console')
self.assertEqual(len(web.containers()), 1)
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(console.containers()), 0)
def test_up_with_recreate(self):
self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
self.assertEqual(len(service.containers()), 1)
old_ids = [c.id for c in service.containers()]
self.command.dispatch(['up', '-d'], None)
self.assertEqual(len(service.containers()), 1)
new_ids = [c.id for c in service.containers()]
self.assertNotEqual(old_ids, new_ids)
def test_up_with_keep_old(self):
self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
self.assertEqual(len(service.containers()), 1)
old_ids = [c.id for c in service.containers()]
self.command.dispatch(['up', '-d', '--no-recreate'], None)
self.assertEqual(len(service.containers()), 1)
new_ids = [c.id for c in service.containers()]
self.assertEqual(old_ids, new_ids)
@patch('fig.packages.dockerpty.start')
def test_run_service_without_links(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['run', 'console', '/bin/true'], None)
self.assertEqual(len(self.project.containers()), 0)
@patch('fig.packages.dockerpty.start')
def test_run_service_with_links(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['run', 'web', '/bin/true'], None)
db = self.project.get_service('db')
console = self.project.get_service('console')
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
@patch('fig.packages.dockerpty.start')
def test_run_with_no_deps(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
db = self.project.get_service('db')
self.assertEqual(len(db.containers()), 0)
@patch('fig.packages.dockerpty.start')
def test_run_does_not_recreate_linked_containers(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['up', '-d', 'db'], None)
db = self.project.get_service('db')
self.assertEqual(len(db.containers()), 1)
old_ids = [c.id for c in db.containers()]
self.command.dispatch(['run', 'web', '/bin/true'], None)
self.assertEqual(len(db.containers()), 1)
new_ids = [c.id for c in db.containers()]
self.assertEqual(old_ids, new_ids)
@patch('fig.packages.dockerpty.start')
def test_run_without_command(self, __):
self.command.base_dir = 'tests/fixtures/commands-figfile'
self.check_build('tests/fixtures/simple-dockerfile', tag='figtest_test')
for c in self.project.containers(stopped=True, one_off=True):
c.remove()
self.command.dispatch(['run', 'implicit'], None)
service = self.project.get_service('implicit')
containers = service.containers(stopped=True, one_off=True)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/sh -c echo "success"'],
)
self.command.dispatch(['run', 'explicit'], None)
service = self.project.get_service('explicit')
containers = service.containers(stopped=True, one_off=True)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/true'],
)
@patch('fig.packages.dockerpty.start')
def test_run_service_with_entrypoint_overridden(self, _):
self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
name = 'service'
self.command.dispatch(
['run', '--entrypoint', '/bin/echo', name, 'helloworld'],
None
)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
self.assertEqual(
container.human_readable_command,
u'/bin/echo helloworld'
)
@patch('fig.packages.dockerpty.start')
def test_run_service_with_environement_overridden(self, _):
name = 'service'
self.command.base_dir = 'tests/fixtures/environment-figfile'
self.command.dispatch(
['run', '-e', 'foo=notbar', '-e', 'allo=moto=bobo',
'-e', 'alpha=beta', name],
None
)
service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=True)[0]
# env overriden
self.assertEqual('notbar', container.environment['foo'])
# keep environement from yaml
self.assertEqual('world', container.environment['hello'])
# added option from command line
self.assertEqual('beta', container.environment['alpha'])
# make sure a value with a = don't crash out
self.assertEqual('moto=bobo', container.environment['allo'])
def test_rm(self):
service = self.project.get_service('simple')
service.create_container()
service.kill()
self.assertEqual(len(service.containers(stopped=True)), 1)
self.command.dispatch(['rm', '--force'], None)
self.assertEqual(len(service.containers(stopped=True)), 0)
def test_restart(self):
service = self.project.get_service('simple')
container = service.create_container()
service.start_container(container)
started_at = container.dictionary['State']['StartedAt']
self.command.dispatch(['restart'], None)
container.inspect()
self.assertNotEqual(
container.dictionary['State']['FinishedAt'],
'0001-01-01T00:00:00Z',
)
self.assertNotEqual(
container.dictionary['State']['StartedAt'],
started_at,
)
def test_scale(self):
project = self.project
self.command.scale(project, {'SERVICE=NUM': ['simple=1']})
self.assertEqual(len(project.get_service('simple').containers()), 1)
self.command.scale(project, {'SERVICE=NUM': ['simple=3', 'another=2']})
self.assertEqual(len(project.get_service('simple').containers()), 3)
self.assertEqual(len(project.get_service('another').containers()), 2)
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
self.assertEqual(len(project.get_service('simple').containers()), 1)
self.assertEqual(len(project.get_service('another').containers()), 1)
self.command.scale(project, {'SERVICE=NUM': ['simple=1', 'another=1']})
self.assertEqual(len(project.get_service('simple').containers()), 1)
self.assertEqual(len(project.get_service('another').containers()), 1)
self.command.scale(project, {'SERVICE=NUM': ['simple=0', 'another=0']})
self.assertEqual(len(project.get_service('simple').containers()), 0)
self.assertEqual(len(project.get_service('another').containers()), 0)
def test_port(self):
self.command.base_dir = 'tests/fixtures/ports-figfile'
self.command.dispatch(['up', '-d'], None)
container = self.project.get_service('simple').get_container()
@patch('sys.stdout', new_callable=StringIO)
def get_port(number, mock_stdout):
self.command.dispatch(['port', 'simple', str(number)], None)
return mock_stdout.getvalue().rstrip()
self.assertEqual(get_port(3000), container.get_local_port(3000))
self.assertEqual(get_port(3001), "0.0.0.0:9999")
self.assertEqual(get_port(3002), "")

View File

@@ -0,0 +1,245 @@
from __future__ import unicode_literals
from fig.project import Project, ConfigurationError
from fig.container import Container
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
def test_volumes_from_service(self):
project = Project.from_config(
name='figtest',
config={
'data': {
'image': 'busybox:latest',
'volumes': ['/var/data'],
},
'db': {
'image': 'busybox:latest',
'volumes_from': ['data'],
},
},
client=self.client,
)
db = project.get_service('db')
data = project.get_service('data')
self.assertEqual(db.volumes_from, [data])
def test_volumes_from_container(self):
data_container = Container.create(
self.client,
image='busybox:latest',
volumes=['/var/data'],
name='figtest_data_container',
)
project = Project.from_config(
name='figtest',
config={
'db': {
'image': 'busybox:latest',
'volumes_from': ['figtest_data_container'],
},
},
client=self.client,
)
db = project.get_service('db')
self.assertEqual(db.volumes_from, [data_container])
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
web_container_1 = web.create_container()
web_container_2 = web.create_container()
db_container = db.create_container()
project.start(service_names=['web'])
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
project.start()
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
project.stop(service_names=['web'], timeout=1)
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
project.kill(service_names=['db'])
self.assertEqual(len(project.containers()), 0)
self.assertEqual(len(project.containers(stopped=True)), 3)
project.remove_stopped(service_names=['web'])
self.assertEqual(len(project.containers(stopped=True)), 1)
project.remove_stopped()
self.assertEqual(len(project.containers(stopped=True)), 0)
def test_project_up(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
self.assertEqual(len(project.containers()), 1)
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(web.containers()), 0)
project.kill()
project.remove_stopped()
def test_project_up_recreates_containers(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/etc'])
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
self.assertEqual(len(project.containers()), 1)
old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].get('Volumes./etc')
project.up()
self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0]
self.assertNotEqual(db_container.id, old_db_id)
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_with_no_recreate_running(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
self.assertEqual(len(project.containers()), 1)
old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
project.up(recreate=False)
self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0]
self.assertEqual(db_container.id, old_db_id)
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_with_no_recreate_stopped(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/var/db'])
project = Project('figtest', [web, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['db'])
project.stop()
old_containers = project.containers(stopped=True)
self.assertEqual(len(old_containers), 1)
old_db_id = old_containers[0].id
db_volume_path = old_containers[0].inspect()['Volumes']['/var/db']
project.up(recreate=False)
new_containers = project.containers(stopped=True)
self.assertEqual(len(new_containers), 2)
db_container = [c for c in new_containers if 'db' in c.name][0]
self.assertEqual(db_container.id, old_db_id)
self.assertEqual(db_container.inspect()['Volumes']['/var/db'],
db_volume_path)
project.kill()
project.remove_stopped()
def test_project_up_without_all_services(self):
console = self.create_service('console')
db = self.create_service('db')
project = Project('figtest', [console, db], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up()
self.assertEqual(len(project.containers()), 2)
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 1)
project.kill()
project.remove_stopped()
def test_project_up_starts_links(self):
console = self.create_service('console')
db = self.create_service('db', volumes=['/var/db'])
web = self.create_service('web', links=[(db, 'db')])
project = Project('figtest', [web, db, console], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['web'])
self.assertEqual(len(project.containers()), 2)
self.assertEqual(len(web.containers()), 1)
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
project.kill()
project.remove_stopped()
def test_project_up_with_no_deps(self):
console = self.create_service('console')
db = self.create_service('db', volumes=['/var/db'])
web = self.create_service('web', links=[(db, 'db')])
project = Project('figtest', [web, db, console], self.client)
project.start()
self.assertEqual(len(project.containers()), 0)
project.up(['web'], start_links=False)
self.assertEqual(len(project.containers()), 1)
self.assertEqual(len(web.containers()), 1)
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(console.containers()), 0)
project.kill()
project.remove_stopped()
def test_unscale_after_restart(self):
web = self.create_service('web')
project = Project('figtest', [web], self.client)
project.start()
service = project.get_service('web')
service.scale(1)
self.assertEqual(len(service.containers()), 1)
service.scale(3)
self.assertEqual(len(service.containers()), 3)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 3)
service.scale(1)
self.assertEqual(len(service.containers()), 1)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 1)
# does scale=0 ,makes any sense? after recreating at least 1 container is running
service.scale(0)
project.up()
service = project.get_service('web')
self.assertEqual(len(service.containers()), 1)
project.kill()
project.remove_stopped()

View File

@@ -0,0 +1,391 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os
from fig import Service
from fig.service import CannotBeScaledError
from fig.container import Container
from docker.errors import APIError
from .testcases import DockerClientTestCase
class ServiceTest(DockerClientTestCase):
def test_containers(self):
foo = self.create_service('foo')
bar = self.create_service('bar')
foo.start_container()
self.assertEqual(len(foo.containers()), 1)
self.assertEqual(foo.containers()[0].name, 'figtest_foo_1')
self.assertEqual(len(bar.containers()), 0)
bar.start_container()
bar.start_container()
self.assertEqual(len(foo.containers()), 1)
self.assertEqual(len(bar.containers()), 2)
names = [c.name for c in bar.containers()]
self.assertIn('figtest_bar_1', names)
self.assertIn('figtest_bar_2', names)
def test_containers_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
self.assertEqual(db.containers(stopped=True), [])
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
def test_project_is_added_to_container_name(self):
service = self.create_service('web')
service.start_container()
self.assertEqual(service.containers()[0].name, 'figtest_web_1')
def test_start_stop(self):
service = self.create_service('scalingtest')
self.assertEqual(len(service.containers(stopped=True)), 0)
service.create_container()
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.start()
self.assertEqual(len(service.containers()), 1)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.stop(timeout=1)
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.stop(timeout=1)
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_kill_remove(self):
service = self.create_service('scalingtest')
service.start_container()
self.assertEqual(len(service.containers()), 1)
service.remove_stopped()
self.assertEqual(len(service.containers()), 1)
service.kill()
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.remove_stopped()
self.assertEqual(len(service.containers(stopped=True)), 0)
def test_create_container_with_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
self.assertEqual(container.name, 'figtest_db_run_1')
def test_create_container_with_one_off_when_existing_container_is_running(self):
db = self.create_service('db')
db.start()
container = db.create_container(one_off=True)
self.assertEqual(container.name, 'figtest_db_run_1')
def test_create_container_with_unspecified_volume(self):
service = self.create_service('db', volumes=['/var/db'])
container = service.create_container()
service.start_container(container)
self.assertIn('/var/db', container.inspect()['Volumes'])
def test_create_container_with_specified_volume(self):
service = self.create_service('db', volumes=['/tmp:/host-tmp'])
container = service.create_container()
service.start_container(container)
self.assertIn('/host-tmp', container.inspect()['Volumes'])
def test_create_container_with_volumes_from(self):
volume_service = self.create_service('data')
volume_container_1 = volume_service.create_container()
volume_container_2 = Container.create(self.client, image='busybox:latest', command=["/bin/sleep", "300"])
host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2])
host_container = host_service.create_container()
host_service.start_container(host_container)
self.assertIn(volume_container_1.id,
host_container.get('HostConfig.VolumesFrom'))
self.assertIn(volume_container_2.id,
host_container.get('HostConfig.VolumesFrom'))
def test_recreate_containers(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/etc'],
entrypoint=['sleep'],
command=['300']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
volume_path = old_container.inspect()['Volumes']['/etc']
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
tuples = service.recreate_containers()
self.assertEqual(len(tuples), 1)
intermediate_container = tuples[0][0]
new_container = tuples[0][1]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['/bin/echo'])
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.name, 'figtest_db_1')
self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path)
self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom'])
self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
self.assertNotEqual(old_container.id, new_container.id)
self.assertRaises(APIError,
self.client.inspect_container,
intermediate_container.id)
def test_recreate_containers_when_containers_are_stopped(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['sleep'],
command=['300']
)
old_container = service.create_container()
self.assertEqual(len(service.containers(stopped=True)), 1)
service.recreate_containers()
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_start_container_passes_through_options(self):
db = self.create_service('db')
db.start_container(environment={'FOO': 'BAR'})
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
def test_start_container_inherits_options_from_constructor(self):
db = self.create_service('db', environment={'FOO': 'BAR'})
db.start_container()
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
def test_start_container_creates_links(self):
db = self.create_service('db')
web = self.create_service('web', links=[(db, None)])
db.start_container()
db.start_container()
web.start_container()
self.assertEqual(
set(web.containers()[0].links()),
set([
'figtest_db_1', 'db_1',
'figtest_db_2', 'db_2',
'db',
]),
)
def test_start_container_creates_links_with_names(self):
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'custom_link_name')])
db.start_container()
db.start_container()
web.start_container()
self.assertEqual(
set(web.containers()[0].links()),
set([
'figtest_db_1', 'db_1',
'figtest_db_2', 'db_2',
'custom_link_name',
]),
)
def test_start_normal_container_does_not_create_links_to_its_own_service(self):
db = self.create_service('db')
db.start_container()
db.start_container()
c = db.start_container()
self.assertEqual(set(c.links()), set([]))
def test_start_one_off_container_creates_links_to_its_own_service(self):
db = self.create_service('db')
db.start_container()
db.start_container()
c = db.start_container(one_off=True)
self.assertEqual(
set(c.links()),
set([
'figtest_db_1', 'db_1',
'figtest_db_2', 'db_2',
'db',
]),
)
def test_start_container_builds_images(self):
service = Service(
name='test',
client=self.client,
build='tests/fixtures/simple-dockerfile',
project='figtest',
)
container = service.start_container()
container.wait()
self.assertIn('success', container.logs())
self.assertEqual(len(self.client.images(name='figtest_test')), 1)
def test_start_container_uses_tagged_image_if_it_exists(self):
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
service = Service(
name='test',
client=self.client,
build='this/does/not/exist/and/will/throw/error',
project='figtest',
)
container = service.start_container()
container.wait()
self.assertIn('success', container.logs())
def test_start_container_creates_ports(self):
service = self.create_service('web', ports=[8000])
container = service.start_container().inspect()
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp'])
self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
def test_start_container_stays_unpriviliged(self):
service = self.create_service('web')
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Privileged'], False)
def test_start_container_becomes_priviliged(self):
service = self.create_service('web', privileged = True)
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Privileged'], True)
def test_expose_does_not_publish_ports(self):
service = self.create_service('web', expose=[8000])
container = service.start_container().inspect()
self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None})
def test_start_container_creates_port_with_explicit_protocol(self):
service = self.create_service('web', ports=['8000/udp'])
container = service.start_container().inspect()
self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp'])
def test_start_container_creates_fixed_external_ports(self):
service = self.create_service('web', ports=['8000:8000'])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000')
def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
service = self.create_service('web', ports=['8001:8000'])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['NetworkSettings']['Ports'])
self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001')
def test_port_with_explicit_interface(self):
service = self.create_service('web', ports=[
'127.0.0.1:8001:8000',
'0.0.0.0:9001:9000/udp',
])
container = service.start_container().inspect()
self.assertEqual(container['NetworkSettings']['Ports'], {
'8000/tcp': [
{
'HostIp': '127.0.0.1',
'HostPort': '8001',
},
],
'9000/udp': [
{
'HostIp': '0.0.0.0',
'HostPort': '9001',
},
],
})
def test_scale(self):
service = self.create_service('web')
service.scale(1)
self.assertEqual(len(service.containers()), 1)
service.scale(3)
self.assertEqual(len(service.containers()), 3)
service.scale(1)
self.assertEqual(len(service.containers()), 1)
service.scale(0)
self.assertEqual(len(service.containers()), 0)
def test_scale_on_service_that_cannot_be_scaled(self):
service = self.create_service('web', ports=['8000:8000'])
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
def test_scale_sets_ports(self):
service = self.create_service('web', ports=['8000'])
service.scale(2)
containers = service.containers()
self.assertEqual(len(containers), 2)
for container in containers:
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
def test_network_mode_none(self):
service = self.create_service('web', net='none')
container = service.start_container()
self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
def test_network_mode_bridged(self):
service = self.create_service('web', net='bridge')
container = service.start_container()
self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
def test_network_mode_host(self):
service = self.create_service('web', net='host')
container = service.start_container()
self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
def test_dns_single_value(self):
service = self.create_service('web', dns='8.8.8.8')
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8'])
def test_dns_list(self):
service = self.create_service('web', dns=['8.8.8.8', '9.9.9.9'])
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8', '9.9.9.9'])
def test_working_dir_param(self):
service = self.create_service('container', working_dir='/working/dir/sample')
container = service.create_container().inspect()
self.assertEqual(container['Config']['WorkingDir'], '/working/dir/sample')
def test_split_env(self):
service = self.create_service('web', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='])
env = service.start_container().environment
for k,v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.iteritems():
self.assertEqual(env[k], v)
def test_resolve_env(self):
service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None})
os.environ['FILE_DEF'] = 'E1'
os.environ['FILE_DEF_EMPTY'] = 'E2'
os.environ['ENV_DEF'] = 'E3'
try:
env = service.start_container().environment
for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.iteritems():
self.assertEqual(env[k], v)
finally:
del os.environ['FILE_DEF']
del os.environ['FILE_DEF_EMPTY']
del os.environ['ENV_DEF']

View File

@@ -0,0 +1,36 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from fig.service import Service
from fig.cli.docker_client import docker_client
from fig.progress_stream import stream_output
from .. import unittest
class DockerClientTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.client = docker_client()
def setUp(self):
for c in self.client.containers(all=True):
if c['Names'] and 'figtest' in c['Names'][0]:
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images():
if isinstance(i.get('Tag'), basestring) and 'figtest' in i['Tag']:
self.client.remove_image(i)
def create_service(self, name, **kwargs):
if 'command' not in kwargs:
kwargs['command'] = ["/bin/sleep", "300"]
return Service(
project='figtest',
name=name,
client=self.client,
image="busybox:latest",
**kwargs
)
def check_build(self, *args, **kwargs):
build_output = self.client.build(*args, **kwargs)
stream_output(build_output, open('/dev/null', 'w'))

View File

@@ -1,124 +0,0 @@
from fig.project import Project
from fig.service import Service
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
def test_from_dict(self):
project = Project.from_dicts('test', [
{
'name': 'web',
'image': 'ubuntu'
},
{
'name': 'db',
'image': 'ubuntu'
}
], self.client)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_dicts('test', [
{
'name': 'web',
'image': 'ubuntu',
'links': ['db'],
},
{
'name': 'db',
'image': 'ubuntu'
}
], self.client)
self.assertEqual(project.services[0].name, 'db')
self.assertEqual(project.services[1].name, 'web')
def test_get_service(self):
web = self.create_service('web')
project = Project('test', [web], self.client)
self.assertEqual(project.get_service('web'), web)
def test_create_containers(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('test', [web, db], self.client)
unstarted = project.create_containers(service_names=['web'])
self.assertEqual(len(unstarted), 1)
self.assertEqual(unstarted[0][0], web)
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 0)
unstarted = project.create_containers()
self.assertEqual(len(unstarted), 2)
self.assertEqual(unstarted[0][0], web)
self.assertEqual(unstarted[1][0], db)
self.assertEqual(len(web.containers(stopped=True)), 2)
self.assertEqual(len(db.containers(stopped=True)), 1)
def test_up(self):
web = self.create_service('web')
db = self.create_service('db')
other = self.create_service('other')
project = Project('test', [web, db, other], self.client)
web.create_container()
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 0)
unstarted = project.create_containers(service_names=['web', 'db'])
self.assertEqual(len(unstarted), 2)
self.assertEqual(unstarted[0][0], web)
self.assertEqual(unstarted[1][0], db)
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(web.containers(stopped=True)), 2)
self.assertEqual(len(db.containers(stopped=True)), 1)
project.kill_and_remove(unstarted)
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 0)
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')
project = Project('test', [web, db], self.client)
project.start()
self.assertEqual(len(web.containers()), 0)
self.assertEqual(len(db.containers()), 0)
web_container_1 = web.create_container()
web_container_2 = web.create_container()
db_container = db.create_container()
project.start(service_names=['web'])
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name]))
project.start()
self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name]))
project.stop(service_names=['web'], timeout=1)
self.assertEqual(set(c.name for c in project.containers()), set([db_container.name]))
project.kill(service_names=['db'])
self.assertEqual(len(project.containers()), 0)
self.assertEqual(len(project.containers(stopped=True)), 3)
project.remove_stopped(service_names=['web'])
self.assertEqual(len(project.containers(stopped=True)), 1)
project.remove_stopped()
self.assertEqual(len(project.containers(stopped=True)), 0)

View File

@@ -1,156 +0,0 @@
from fig import Service
from .testcases import DockerClientTestCase
class ServiceTest(DockerClientTestCase):
def test_name_validations(self):
self.assertRaises(ValueError, lambda: Service(name=''))
self.assertRaises(ValueError, lambda: Service(name=' '))
self.assertRaises(ValueError, lambda: Service(name='/'))
self.assertRaises(ValueError, lambda: Service(name='!'))
self.assertRaises(ValueError, lambda: Service(name='\xe2'))
self.assertRaises(ValueError, lambda: Service(name='_'))
self.assertRaises(ValueError, lambda: Service(name='____'))
self.assertRaises(ValueError, lambda: Service(name='foo_bar'))
self.assertRaises(ValueError, lambda: Service(name='__foo_bar__'))
Service('a')
Service('foo')
def test_project_validation(self):
self.assertRaises(ValueError, lambda: Service(name='foo', project='_'))
Service(name='foo', project='bar')
def test_containers(self):
foo = self.create_service('foo')
bar = self.create_service('bar')
foo.start_container()
self.assertEqual(len(foo.containers()), 1)
self.assertEqual(foo.containers()[0].name, 'default_foo_1')
self.assertEqual(len(bar.containers()), 0)
bar.start_container()
bar.start_container()
self.assertEqual(len(foo.containers()), 1)
self.assertEqual(len(bar.containers()), 2)
names = [c.name for c in bar.containers()]
self.assertIn('default_bar_1', names)
self.assertIn('default_bar_2', names)
def test_containers_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
self.assertEqual(db.containers(stopped=True), [])
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
def test_project_is_added_to_container_name(self):
service = self.create_service('web', project='myproject')
service.start_container()
self.assertEqual(service.containers()[0].name, 'myproject_web_1')
def test_start_stop(self):
service = self.create_service('scalingtest')
self.assertEqual(len(service.containers(stopped=True)), 0)
service.create_container()
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.start()
self.assertEqual(len(service.containers()), 1)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.stop(timeout=1)
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.stop(timeout=1)
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_kill_remove(self):
service = self.create_service('scalingtest')
service.start_container()
self.assertEqual(len(service.containers()), 1)
service.remove_stopped()
self.assertEqual(len(service.containers()), 1)
service.kill()
self.assertEqual(len(service.containers()), 0)
self.assertEqual(len(service.containers(stopped=True)), 1)
service.remove_stopped()
self.assertEqual(len(service.containers(stopped=True)), 0)
def test_create_container_with_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
self.assertEqual(container.name, 'default_db_run_1')
def test_create_container_with_one_off_when_existing_container_is_running(self):
db = self.create_service('db')
db.start()
container = db.create_container(one_off=True)
self.assertEqual(container.name, 'default_db_run_1')
def test_start_container_passes_through_options(self):
db = self.create_service('db')
db.start_container(environment={'FOO': 'BAR'})
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
def test_start_container_inherits_options_from_constructor(self):
db = self.create_service('db', environment={'FOO': 'BAR'})
db.start_container()
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
def test_start_container_creates_links(self):
db = self.create_service('db')
web = self.create_service('web', links=[db])
db.start_container()
web.start_container()
self.assertIn('default_db_1', web.containers()[0].links())
db.stop(timeout=1)
web.stop(timeout=1)
def test_start_container_builds_images(self):
service = Service(
name='test',
client=self.client,
build='tests/fixtures/simple-dockerfile',
)
container = service.start_container()
container.wait()
self.assertIn('success', container.logs())
self.assertEqual(len(self.client.images(name='default_test')), 1)
def test_start_container_uses_tagged_image_if_it_exists(self):
self.client.build('tests/fixtures/simple-dockerfile', tag='default_test')
service = Service(
name='test',
client=self.client,
build='this/does/not/exist/and/will/throw/error',
)
container = service.start_container()
container.wait()
self.assertIn('success', container.logs())
def test_start_container_creates_ports(self):
service = self.create_service('web', ports=[8000])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
def test_start_container_creates_fixed_external_ports(self):
service = self.create_service('web', ports=['8000:8000'])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')

View File

@@ -1,31 +0,0 @@
from docker import Client
from fig.service import Service
import os
from unittest import TestCase
class DockerClientTestCase(TestCase):
@classmethod
def setUpClass(cls):
if os.environ.get('DOCKER_URL'):
cls.client = Client(os.environ['DOCKER_URL'])
else:
cls.client = Client()
cls.client.pull('ubuntu')
def setUp(self):
for c in self.client.containers(all=True):
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
def create_service(self, name, **kwargs):
return Service(
name=name,
client=self.client,
image="ubuntu",
command=["/bin/sleep", "300"],
**kwargs
)

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

View File

View File

@@ -0,0 +1,30 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from tests import unittest
from fig.cli import verbose_proxy
class VerboseProxy(unittest.TestCase):
def test_format_call(self):
expected = "(u'arg1', True, key=u'value')"
actual = verbose_proxy.format_call(
("arg1", True),
{'key': 'value'})
self.assertEqual(expected, actual)
def test_format_return_sequence(self):
expected = "(list with 10 items)"
actual = verbose_proxy.format_return(list(range(10)), 2)
self.assertEqual(expected, actual)
def test_format_return(self):
expected = "{u'Id': u'ok'}"
actual = verbose_proxy.format_return({'Id': 'ok'}, 2)
self.assertEqual(expected, actual)
def test_format_return_no_result(self):
actual = verbose_proxy.format_return(None, 2)
self.assertEqual(None, actual)

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

@@ -0,0 +1,69 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
import os
from .. import unittest
import mock
from fig.cli import main
from fig.cli.main import TopLevelCommand
from six import StringIO
class CLITestCase(unittest.TestCase):
def test_default_project_name(self):
cwd = os.getcwd()
try:
os.chdir('tests/fixtures/simple-figfile')
command = TopLevelCommand()
project_name = command.get_project_name(command.get_config_path())
self.assertEquals('simplefigfile', project_name)
finally:
os.chdir(cwd)
def test_project_name_with_explicit_base_dir(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/simple-figfile'
project_name = command.get_project_name(command.get_config_path())
self.assertEquals('simplefigfile', project_name)
def test_project_name_with_explicit_project_name(self):
command = TopLevelCommand()
name = 'explicit-project-name'
project_name = command.get_project_name(None, project_name=name)
self.assertEquals('explicitprojectname', project_name)
def test_project_name_from_environment(self):
command = TopLevelCommand()
name = 'namefromenv'
with mock.patch.dict(os.environ):
os.environ['FIG_PROJECT_NAME'] = name
project_name = command.get_project_name(None)
self.assertEquals(project_name, name)
def test_yaml_filename_check(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-figfile'
with mock.patch('fig.cli.command.log', autospec=True) as mock_log:
self.assertTrue(command.get_config_path())
self.assertEqual(mock_log.warning.call_count, 2)
def test_get_project(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-figfile'
project = command.get_project(command.get_config_path())
self.assertEqual(project.name, 'longerfilenamefigfile')
self.assertTrue(project.client)
self.assertTrue(project.services)
def test_help(self):
command = TopLevelCommand()
with self.assertRaises(SystemExit):
command.dispatch(['-h'], None)
def test_setup_logging(self):
main.setup_logging()
self.assertEqual(logging.getLogger().level, logging.DEBUG)
self.assertEqual(logging.getLogger('requests').propagate, False)

View File

@@ -0,0 +1,119 @@
from __future__ import unicode_literals
from .. import unittest
import mock
import docker
from fig.container import Container
class ContainerTest(unittest.TestCase):
def setUp(self):
self.container_dict = {
"Id": "abc",
"Image": "busybox:latest",
"Command": "sleep 300",
"Created": 1387384730,
"Status": "Up 8 seconds",
"Ports": None,
"SizeRw": 0,
"SizeRootFs": 0,
"Names": ["/figtest_db_1"],
"NetworkSettings": {
"Ports": {},
},
}
def test_from_ps(self):
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.dictionary, {
"Id": "abc",
"Image":"busybox:latest",
"Name": "/figtest_db_1",
})
def test_environment(self):
container = Container(None, {
'Id': 'abc',
'Config': {
'Env': [
'FOO=BAR',
'BAZ=DOGE',
]
}
}, has_been_inspected=True)
self.assertEqual(container.environment, {
'FOO': 'BAR',
'BAZ': 'DOGE',
})
def test_number(self):
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.number, 1)
def test_name(self):
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.name, "figtest_db_1")
def test_name_without_project(self):
container = Container.from_ps(None,
self.container_dict,
has_been_inspected=True)
self.assertEqual(container.name_without_project, "db_1")
def test_inspect_if_not_inspected(self):
mock_client = mock.create_autospec(docker.Client)
container = Container(mock_client, dict(Id="the_id"))
container.inspect_if_not_inspected()
mock_client.inspect_container.assert_called_once_with("the_id")
self.assertEqual(container.dictionary,
mock_client.inspect_container.return_value)
self.assertTrue(container.has_been_inspected)
container.inspect_if_not_inspected()
self.assertEqual(mock_client.inspect_container.call_count, 1)
def test_human_readable_ports_none(self):
container = Container(None, self.container_dict, has_been_inspected=True)
self.assertEqual(container.human_readable_ports, '')
def test_human_readable_ports_public_and_private(self):
self.container_dict['NetworkSettings']['Ports'].update({
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
"45453/tcp": [],
})
container = Container(None, self.container_dict, has_been_inspected=True)
expected = "45453/tcp, 0.0.0.0:49197->45454/tcp"
self.assertEqual(container.human_readable_ports, expected)
def test_get_local_port(self):
self.container_dict['NetworkSettings']['Ports'].update({
"45454/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "49197" } ],
})
container = Container(None, self.container_dict, has_been_inspected=True)
self.assertEqual(
container.get_local_port(45454, protocol='tcp'),
'0.0.0.0:49197')
def test_get(self):
container = Container(None, {
"Status":"Up 8 seconds",
"HostConfig": {
"VolumesFrom": ["volume_id",]
},
}, has_been_inspected=True)
self.assertEqual(container.get('Status'), "Up 8 seconds")
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id",])
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)

View File

@@ -0,0 +1,69 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os
from fig.cli.log_printer import LogPrinter
from .. import unittest
class LogPrinterTest(unittest.TestCase):
def get_default_output(self, monochrome=False):
def reader(*args, **kwargs):
yield "hello\nworld"
container = MockContainer(reader)
output = run_log_printer([container], monochrome=monochrome)
return output
def test_single_container(self):
output = self.get_default_output()
self.assertIn('hello', output)
self.assertIn('world', output)
def test_monochrome(self):
output = self.get_default_output(monochrome=True)
self.assertNotIn('\033[', output)
def test_polychrome(self):
output = self.get_default_output()
self.assertIn('\033[', output)
def test_unicode(self):
glyph = u'\u2022'.encode('utf-8')
def reader(*args, **kwargs):
yield glyph + b'\n'
container = MockContainer(reader)
output = run_log_printer([container])
self.assertIn(glyph, output)
def run_log_printer(containers, monochrome=False):
r, w = os.pipe()
reader, writer = os.fdopen(r, 'r'), os.fdopen(w, 'w')
printer = LogPrinter(containers, output=writer, monochrome=monochrome)
printer.run()
writer.close()
return reader.read()
class MockContainer(object):
def __init__(self, reader):
self._reader = reader
@property
def name(self):
return 'myapp_web_1'
@property
def name_without_project(self):
return 'web_1'
def attach(self, *args, **kwargs):
return self._reader()
def wait(self, *args, **kwargs):
return 0

141
tests/unit/project_test.py Normal file
View File

@@ -0,0 +1,141 @@
from __future__ import unicode_literals
from .. import unittest
from fig.service import Service
from fig.project import Project, ConfigurationError
class ProjectTest(unittest.TestCase):
def test_from_dict(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'busybox:latest'
},
{
'name': 'db',
'image': 'busybox:latest'
},
], None)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_dicts('figtest', [
{
'name': 'web',
'image': 'busybox:latest',
'links': ['db'],
},
{
'name': 'db',
'image': 'busybox:latest',
'volumes_from': ['volume']
},
{
'name': 'volume',
'image': 'busybox:latest',
'volumes': ['/tmp'],
}
], None)
self.assertEqual(project.services[0].name, 'volume')
self.assertEqual(project.services[1].name, 'db')
self.assertEqual(project.services[2].name, 'web')
def test_from_config(self):
project = Project.from_config('figtest', {
'web': {
'image': 'busybox:latest',
},
'db': {
'image': 'busybox:latest',
},
}, None)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
def test_from_config_throws_error_when_not_dict(self):
with self.assertRaises(ConfigurationError):
project = Project.from_config('figtest', {
'web': 'busybox:latest',
}, None)
def test_get_service(self):
web = Service(
project='figtest',
name='web',
client=None,
image="busybox:latest",
)
project = Project('test', [web], None)
self.assertEqual(project.get_service('web'), web)
def test_get_services_returns_all_services_without_args(self):
web = Service(
project='figtest',
name='web',
)
console = Service(
project='figtest',
name='console',
)
project = Project('test', [web, console], None)
self.assertEqual(project.get_services(), [web, console])
def test_get_services_returns_listed_services_with_args(self):
web = Service(
project='figtest',
name='web',
)
console = Service(
project='figtest',
name='console',
)
project = Project('test', [web, console], None)
self.assertEqual(project.get_services(['console']), [console])
def test_get_services_with_include_links(self):
db = Service(
project='figtest',
name='db',
)
web = Service(
project='figtest',
name='web',
links=[(db, 'database')]
)
cache = Service(
project='figtest',
name='cache'
)
console = Service(
project='figtest',
name='console',
links=[(web, 'web')]
)
project = Project('test', [web, db, cache, console], None)
self.assertEqual(
project.get_services(['console'], include_links=True),
[db, web, console]
)
def test_get_services_removes_duplicates_following_links(self):
db = Service(
project='figtest',
name='db',
)
web = Service(
project='figtest',
name='web',
links=[(db, 'database')]
)
project = Project('test', [web, db], None)
self.assertEqual(
project.get_services(['web', 'db'], include_links=True),
[db, web]
)

218
tests/unit/service_test.py Normal file
View File

@@ -0,0 +1,218 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import os
from .. import unittest
import mock
import docker
from fig import Service
from fig.container import Container
from fig.service import (
ConfigError,
split_port,
parse_volume_spec,
build_volume_binding,
)
class ServiceTest(unittest.TestCase):
def setUp(self):
self.mock_client = mock.create_autospec(docker.Client)
def test_name_validations(self):
self.assertRaises(ConfigError, lambda: Service(name=''))
self.assertRaises(ConfigError, lambda: Service(name=' '))
self.assertRaises(ConfigError, lambda: Service(name='/'))
self.assertRaises(ConfigError, lambda: Service(name='!'))
self.assertRaises(ConfigError, lambda: Service(name='\xe2'))
self.assertRaises(ConfigError, lambda: Service(name='_'))
self.assertRaises(ConfigError, lambda: Service(name='____'))
self.assertRaises(ConfigError, lambda: Service(name='foo_bar'))
self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__'))
Service('a')
Service('foo')
def test_project_validation(self):
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_'))
Service(name='foo', project='bar')
def test_config_validation(self):
self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000']))
Service(name='foo', ports=['8000'])
def test_get_volumes_from_container(self):
container_id = 'aabbccddee'
service = Service(
'test',
volumes_from=[mock.Mock(id=container_id, spec=Container)])
self.assertEqual(service._get_volumes_from(), [container_id])
def test_get_volumes_from_intermediate_container(self):
container_id = 'aabbccddee'
service = Service('test')
container = mock.Mock(id=container_id, spec=Container)
self.assertEqual(service._get_volumes_from(container), [container_id])
def test_get_volumes_from_service_container_exists(self):
container_ids = ['aabbccddee', '12345']
from_service = mock.create_autospec(Service)
from_service.containers.return_value = [
mock.Mock(id=container_id, spec=Container)
for container_id in container_ids
]
service = Service('test', volumes_from=[from_service])
self.assertEqual(service._get_volumes_from(), container_ids)
def test_get_volumes_from_service_no_container(self):
container_id = 'abababab'
from_service = mock.create_autospec(Service)
from_service.containers.return_value = []
from_service.create_container.return_value = mock.Mock(
id=container_id,
spec=Container)
service = Service('test', volumes_from=[from_service])
self.assertEqual(service._get_volumes_from(), [container_id])
from_service.create_container.assert_called_once_with()
def test_split_port_with_host_ip(self):
internal_port, external_port = split_port("127.0.0.1:1000:2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, ("127.0.0.1", "1000"))
def test_split_port_with_protocol(self):
internal_port, external_port = split_port("127.0.0.1:1000:2000/udp")
self.assertEqual(internal_port, "2000/udp")
self.assertEqual(external_port, ("127.0.0.1", "1000"))
def test_split_port_with_host_ip_no_port(self):
internal_port, external_port = split_port("127.0.0.1::2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, ("127.0.0.1", None))
def test_split_port_with_host_port(self):
internal_port, external_port = split_port("1000:2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, "1000")
def test_split_port_no_host_port(self):
internal_port, external_port = split_port("2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, None)
def test_split_port_invalid(self):
with self.assertRaises(ConfigError):
split_port("0.0.0.0:1000:2000:tcp")
def test_split_domainname_none(self):
service = Service('foo', hostname='name', client=self.mock_client)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name', 'hostname')
self.assertFalse('domainname' in opts, 'domainname')
def test_split_domainname_fqdn(self):
service = Service('foo',
hostname='name.domain.tld',
client=self.mock_client)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
def test_split_domainname_both(self):
service = Service('foo',
hostname='name',
domainname='domain.tld',
client=self.mock_client)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
def test_split_domainname_weird(self):
service = Service('foo',
hostname='name.sub',
domainname='domain.tld',
client=self.mock_client)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
def test_get_container_not_found(self):
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = []
service = Service('foo', client=mock_client)
self.assertRaises(ValueError, service.get_container)
@mock.patch('fig.service.Container', autospec=True)
def test_get_container(self, mock_container_class):
mock_client = mock.create_autospec(docker.Client)
container_dict = dict(Name='default_foo_2')
mock_client.containers.return_value = [container_dict]
service = Service('foo', client=mock_client)
container = service.get_container(number=2)
self.assertEqual(container, mock_container_class.from_ps.return_value)
mock_container_class.from_ps.assert_called_once_with(
mock_client, container_dict)
@mock.patch('fig.service.log', autospec=True)
def test_pull_image(self, mock_log):
service = Service('foo', client=self.mock_client, image='someimage:sometag')
service.pull(insecure_registry=True)
self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True)
mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
class ServiceVolumesTest(unittest.TestCase):
def test_parse_volume_spec_only_one_path(self):
spec = parse_volume_spec('/the/volume')
self.assertEqual(spec, (None, '/the/volume', 'rw'))
def test_parse_volume_spec_internal_and_external(self):
spec = parse_volume_spec('external:interval')
self.assertEqual(spec, ('external', 'interval', 'rw'))
def test_parse_volume_spec_with_mode(self):
spec = parse_volume_spec('external:interval:ro')
self.assertEqual(spec, ('external', 'interval', 'ro'))
def test_parse_volume_spec_too_many_parts(self):
with self.assertRaises(ConfigError):
parse_volume_spec('one:two:three:four')
def test_parse_volume_bad_mode(self):
with self.assertRaises(ConfigError):
parse_volume_spec('one:two:notrw')
def test_build_volume_binding(self):
binding = build_volume_binding(parse_volume_spec('/outside:/inside'))
self.assertEqual(
binding,
('/outside', dict(bind='/inside', ro=False)))
@mock.patch.dict(os.environ)
def test_build_volume_binding_with_environ(self):
os.environ['VOLUME_PATH'] = '/opt'
binding = build_volume_binding(parse_volume_spec('${VOLUME_PATH}:/opt'))
self.assertEqual(binding, ('/opt', dict(bind='/opt', ro=False)))
@mock.patch.dict(os.environ)
def test_building_volume_binding_with_home(self):
os.environ['HOME'] = '/home/user'
binding = build_volume_binding(parse_volume_spec('~:/home/user'))
self.assertEqual(
binding,
('/home/user', dict(bind='/home/user', ro=False)))

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,52 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from fig.cli.utils import split_buffer
from .. import unittest
class SplitBufferTest(unittest.TestCase):
def test_single_line_chunks(self):
def reader():
yield b'abc\n'
yield b'def\n'
yield b'ghi\n'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi\n'])
def test_no_end_separator(self):
def reader():
yield b'abc\n'
yield b'def\n'
yield b'ghi'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
def test_multiple_line_chunk(self):
def reader():
yield b'abc\ndef\nghi'
self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi'])
def test_chunked_line(self):
def reader():
yield b'a'
yield b'b'
yield b'c'
yield b'\n'
yield b'd'
self.assert_produces(reader, [b'abc\n', b'd'])
def test_preserves_unicode_sequences_within_lines(self):
string = u"a\u2022c\n".encode('utf-8')
def reader():
yield string
self.assert_produces(reader, [string])
def assert_produces(self, reader, expectations):
split = split_buffer(reader(), b'\n')
for (actual, expected) in zip(split, expectations):
self.assertEqual(type(actual), type(expected))
self.assertEqual(actual, expected)

15
tox.ini Normal file
View File

@@ -0,0 +1,15 @@
[tox]
envlist = py26,py27,py32,py33,pypy
[testenv]
usedevelop=True
deps =
-rrequirements.txt
-rrequirements-dev.txt
commands =
nosetests {posargs}
flake8 fig
[flake8]
# ignore line-length for now
ignore = E501,E203

12
wercker.yml Normal file
View File

@@ -0,0 +1,12 @@
box: wercker-labs/docker
build:
steps:
- script:
name: validate DCO
code: script/validate-dco
- script:
name: run tests
code: script/test
- script:
name: build binary
code: script/build-linux