Compare commits

...

99 Commits
0.4.2 ... 0.5.1

Author SHA1 Message Date
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
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
d11wtq
6e485df084 Rename --no-links to --only
Signed-off-by: d11wtq <chris@w3style.co.uk>
2014-06-11 10:50:45 +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
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
31 changed files with 1127 additions and 312 deletions

View File

@@ -1,6 +1,55 @@
Change log
==========
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)
------------------

View File

@@ -10,7 +10,7 @@ that should get you started.
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
1. Enter the local directory `cd fig`.
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases.
1. 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
@@ -73,3 +73,21 @@ 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
5. Publish GitHub release, creating tag

View File

@@ -1,4 +1,4 @@
FROM stackbrew/ubuntu:13.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/

View File

@@ -45,7 +45,7 @@ For example:
$ fig run web python manage.py shell
Note that this will not start any services that the command's service links to. So if, for example, your one-off command talks to your database, you will need to run `fig up -d db` first.
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.
@@ -53,6 +53,10 @@ Links are also created between one-off commands and the other containers for tha
$ fig run db /bin/sh -c "psql -h \$DB_1_PORT_5432_TCP_ADDR -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.
@@ -74,8 +78,10 @@ Stop running containers without removing them. They can be started again with `f
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.
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.
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/

View File

@@ -113,8 +113,8 @@ Now if we run `fig up`, it'll pull a Redis image, build an image for our own cod
Building web...
Starting figtest_redis_1...
Starting figtest_web_1...
figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
figtest_web_1 | * Running on http://0.0.0.0:5000/
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/
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!

View File

@@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun
Next, install Fig. On OS X:
$ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.1/darwin > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
On 64-bit Linux:
$ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.1/linux > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ class Command(DocoptCommand):
base_dir = '.'
def __init__(self):
self.yaml_path = os.environ.get('FIG_FILE', None)
self._yaml_path = os.environ.get('FIG_FILE', None)
self.explicit_project_name = None
def dispatch(self, *args, **kwargs):
@@ -56,10 +56,7 @@ class Command(DocoptCommand):
@cached_property
def project(self):
try:
yaml_path = self.yaml_path
if yaml_path is None:
yaml_path = self.check_yaml_filename()
config = yaml.load(open(yaml_path))
config = yaml.safe_load(open(self.yaml_path))
except IOError as e:
if e.errno == errno.ENOENT:
raise errors.FigFileNotFound(os.path.basename(e.filename))
@@ -72,7 +69,7 @@ class Command(DocoptCommand):
@cached_property
def project_name(self):
project = os.path.basename(os.getcwd())
project = os.path.basename(os.path.dirname(os.path.abspath(self.yaml_path)))
if self.explicit_project_name is not None:
project = self.explicit_project_name
project = re.sub(r'[^a-zA-Z0-9]', '', project)
@@ -84,8 +81,11 @@ class Command(DocoptCommand):
def formatter(self):
return Formatter()
def check_yaml_filename(self):
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
@cached_property
def yaml_path(self):
if self._yaml_path is not None:
return self._yaml_path
elif 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")
@@ -93,3 +93,7 @@ class Command(DocoptCommand):
return os.path.join(self.base_dir, 'fig.yaml')
else:
return os.path.join(self.base_dir, 'fig.yml')
@yaml_path.setter
def yaml_path(self, value):
self._yaml_path = value

View File

@@ -6,6 +6,7 @@ import re
import signal
from inspect import getdoc
import dockerpty
from .. import __version__
from ..project import NoSuchService, ConfigurationError
@@ -18,7 +19,6 @@ from .utils import yesno
from ..packages.docker.errors import APIError
from .errors import UserError
from .docopt_command import NoSuchCommand
from .socketclient import SocketClient
log = logging.getLogger(__name__)
@@ -202,27 +202,44 @@ class TopLevelCommand(Command):
$ fig run web python manage.py shell
Note that this will not start any services that the command's service
links to. So if, for example, your one-off command talks to your
database, you will need to run `fig up -d db` first.
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] SERVICE COMMAND [ARGS...]
Usage: run [options] SERVICE [COMMAND] [ARGS...]
Options:
-d Detached mode: Run container in the background, print new
container name
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
--rm Remove container after run. Ignored in detached mode.
-d Detached mode: Run container in the background, print
new container name.
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
--rm Remove container after run. Ignored in detached mode.
--no-deps Don't start linked services.
"""
service = self.project.get_service(options['SERVICE'])
if not options['--no-deps']:
deps = service.get_linked_names()
if len(deps) > 0:
self.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'],
'command': command,
'tty': tty,
'stdin_open': not options['-d'],
}
@@ -231,9 +248,8 @@ class TopLevelCommand(Command):
service.start_container(container, ports=None, one_off=True)
print(container.name)
else:
with self._attach_to_container(container.id, raw=tty) as c:
service.start_container(container, ports=None, one_off=True)
c.run()
service.start_container(container, ports=None, one_off=True)
dockerpty.start(self.client, container.id)
exit_code = container.wait()
if options['--rm']:
log.info("Removing %s..." % container.name)
@@ -293,17 +309,31 @@ class TopLevelCommand(Command):
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.
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
-d Detached mode: Run containers in the background,
print new container names.
--no-deps Don't start linked services.
--no-recreate If containers already exist, don't recreate them.
"""
detached = options['-d']
to_attach = self.project.up(service_names=options['SERVICE'])
start_links = not options['--no-deps']
recreate = not options['--no-recreate']
service_names = options['SERVICE']
self.project.up(
service_names=service_names,
start_links=start_links,
recreate=recreate
)
to_attach = [c for s in self.project.get_services(service_names) for c in s.containers()]
if not detached:
print("Attaching to", list_containers(to_attach))
@@ -313,24 +343,12 @@ class TopLevelCommand(Command):
log_printer.run()
finally:
def handler(signal, frame):
self.project.kill(service_names=options['SERVICE'])
self.project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
print("Gracefully stopping... (press Ctrl+C again to force)")
self.project.stop(service_names=options['SERVICE'])
def _attach_to_container(self, container_id, raw=False):
socket_in = self.client.attach_socket(container_id, params={'stdin': 1, 'stream': 1})
socket_out = self.client.attach_socket(container_id, params={'stdout': 1, 'logs': 1, 'stream': 1})
socket_err = self.client.attach_socket(container_id, params={'stderr': 1, 'logs': 1, 'stream': 1})
return SocketClient(
socket_in=socket_in,
socket_out=socket_out,
socket_err=socket_err,
raw=raw,
)
self.project.stop(service_names=service_names)
def list_containers(containers):
return ", ".join(c.name for c in containers)

View File

@@ -1,126 +0,0 @@
from __future__ import print_function
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
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, 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, args=(self.socket_out, sys.stdout)))
if self.socket_err is not None:
recv_threads.append(self.start_background_thread(target=self.recv, 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(self, socket, stream):
try:
while True:
chunk = socket.recv(4096)
if chunk:
stream.write(chunk)
stream.flush()
else:
break
except Exception as e:
log.debug(e)
def send(self, socket, stream):
while True:
chunk = stream.read(1)
if chunk == '':
socket.close()
break
else:
try:
socket.send(chunk)
except Exception as 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")
sys.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

@@ -17,7 +17,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', []):
@@ -36,7 +36,7 @@ class Container(object):
@property
def id(self):
return self.dictionary['ID']
return self.dictionary['Id']
@property
def image(self):

View File

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

View File

@@ -16,6 +16,7 @@ import json
import re
import shlex
import struct
import warnings
import requests
import requests.exceptions
@@ -29,7 +30,7 @@ from . import errors
if not six.PY3:
import websocket
DEFAULT_DOCKER_API_VERSION = '1.9'
DEFAULT_DOCKER_API_VERSION = '1.12'
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
@@ -95,7 +96,8 @@ class Client(requests.Session):
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, entrypoint=None,
cpu_shares=None, working_dir=None, domainname=None):
cpu_shares=None, working_dir=None, domainname=None,
memswap_limit=0):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
@@ -121,8 +123,12 @@ class Client(requests.Session):
volumes_dict[vol] = {}
volumes = volumes_dict
if volumes_from and not isinstance(volumes_from, six.string_types):
volumes_from = ','.join(volumes_from)
if volumes_from:
if not isinstance(volumes_from, six.string_types):
volumes_from = ','.join(volumes_from)
else:
# Force None, an empty list or dict causes client.start to fail
volumes_from = None
attach_stdin = False
attach_stdout = False
@@ -137,6 +143,14 @@ class Client(requests.Session):
attach_stdin = True
stdin_once = True
if utils.compare_version('1.10', self._version) >= 0:
message = ('{0!r} parameter has no effect on create_container().'
' It has been moved to start()')
if dns is not None:
raise errors.DockerException(message.format('dns'))
if volumes_from is not None:
raise errors.DockerException(message.format('volumes_from'))
return {
'Hostname': hostname,
'Domainname': domainname,
@@ -158,7 +172,8 @@ class Client(requests.Session):
'NetworkDisabled': network_disabled,
'Entrypoint': entrypoint,
'CpuShares': cpu_shares,
'WorkingDir': working_dir
'WorkingDir': working_dir,
'MemorySwap': memswap_limit
}
def _post_json(self, url, data, **kwargs):
@@ -235,7 +250,7 @@ class Client(requests.Session):
start = walker + STREAM_HEADER_SIZE_BYTES
end = start + length
walker = end
yield str(buf[start:end])
yield buf[start:end]
def _multiplexed_socket_stream_helper(self, response):
"""A generator of multiplexed data blocks coming from a response
@@ -296,8 +311,10 @@ class Client(requests.Session):
return stream_result() if stream else \
self._result(response, binary=True)
sep = bytes() if six.PY3 else str()
return stream and self._multiplexed_socket_stream_helper(response) or \
''.join([x for x in self._multiplexed_buffer_helper(response)])
sep.join([x for x in self._multiplexed_buffer_helper(response)])
def attach_socket(self, container, params=None, ws=False):
if params is None:
@@ -318,14 +335,20 @@ class Client(requests.Session):
u, None, params=self._attach_params(params), stream=True))
def build(self, path=None, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, stream=False, timeout=None):
nocache=False, rm=False, stream=False, timeout=None,
custom_context=False, encoding=None):
remote = context = headers = None
if path is None and fileobj is None:
raise TypeError("Either path or fileobj needs to be provided.")
if fileobj is not None:
if custom_context:
if not fileobj:
raise TypeError("You must specify fileobj with custom_context")
context = fileobj
elif fileobj is not None:
context = utils.mkbuildcontext(fileobj)
elif path.startswith(('http://', 'https://', 'git://', 'github.com/')):
elif path.startswith(('http://', 'https://',
'git://', 'github.com/')):
remote = path
else:
context = utils.tar(path)
@@ -341,8 +364,11 @@ class Client(requests.Session):
'nocache': nocache,
'rm': rm
}
if context is not None:
headers = {'Content-Type': 'application/tar'}
if encoding:
headers['Content-Encoding'] = encoding
if utils.compare_version('1.9', self._version) >= 0:
# If we don't have any auth data so far, try reloading the config
@@ -393,10 +419,11 @@ class Client(requests.Session):
json=True)
def containers(self, quiet=False, all=False, trunc=True, latest=False,
since=None, before=None, limit=-1):
since=None, before=None, limit=-1, size=False):
params = {
'limit': 1 if latest else limit,
'all': 1 if all else 0,
'size': 1 if size else 0,
'trunc_cmd': 1 if trunc else 0,
'since': since,
'before': before
@@ -424,12 +451,13 @@ class Client(requests.Session):
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, name=None, entrypoint=None,
cpu_shares=None, working_dir=None, domainname=None):
cpu_shares=None, working_dir=None, domainname=None,
memswap_limit=0):
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from, network_disabled,
entrypoint, cpu_shares, working_dir, domainname
entrypoint, cpu_shares, working_dir, domainname, memswap_limit
)
return self.create_container_from_config(config, name)
@@ -458,6 +486,12 @@ class Client(requests.Session):
self._raise_for_status(res)
return res.raw
def get_image(self, image):
res = self._get(self._url("/images/{0}/get".format(image)),
stream=True)
self._raise_for_status(res)
return res.raw
def history(self, image):
res = self._get(self._url("/images/{0}/history".format(image)))
self._raise_for_status(res)
@@ -513,6 +547,10 @@ class Client(requests.Session):
True)
def insert(self, image, url, path):
if utils.compare_version('1.12', self._version) >= 0:
raise errors.DeprecatedMethod(
'insert is not available for API version >=1.12'
)
api_url = self._url("/images/" + image + "/insert")
params = {
'url': url,
@@ -544,6 +582,10 @@ class Client(requests.Session):
self._raise_for_status(res)
def load_image(self, data):
res = self._post(self._url("/images/load"), data=data)
self._raise_for_status(res)
def login(self, username, password=None, email=None, registry=None,
reauth=False):
# If we don't have any auth data so far, try reloading the config file
@@ -572,7 +614,27 @@ class Client(requests.Session):
self._auth_configs[registry] = req_data
return self._result(response, json=True)
def logs(self, container, stdout=True, stderr=True, stream=False):
def logs(self, container, stdout=True, stderr=True, stream=False,
timestamps=False):
if isinstance(container, dict):
container = container.get('Id')
if utils.compare_version('1.11', self._version) >= 0:
params = {'stderr': stderr and 1 or 0,
'stdout': stdout and 1 or 0,
'timestamps': timestamps and 1 or 0,
'follow': stream and 1 or 0}
url = self._url("/containers/{0}/logs".format(container))
res = self._get(url, params=params, stream=stream)
if stream:
return self._multiplexed_socket_stream_helper(res)
elif six.PY3:
return bytes().join(
[x for x in self._multiplexed_buffer_helper(res)]
)
else:
return str().join(
[x for x in self._multiplexed_buffer_helper(res)]
)
return self.attach(
container,
stdout=stdout,
@@ -581,6 +643,9 @@ class Client(requests.Session):
logs=True
)
def ping(self):
return self._result(self._get(self._url('/_ping')))
def port(self, container, private_port):
if isinstance(container, dict):
container = container.get('Id')
@@ -597,6 +662,8 @@ class Client(requests.Session):
return h_ports
def pull(self, repository, tag=None, stream=False):
if not tag:
repository, tag = utils.parse_repository_tag(repository)
registry, repo_name = auth.resolve_repository_name(repository)
if repo_name.count(":") == 1:
repository, tag = repository.rsplit(":", 1)
@@ -653,16 +720,17 @@ class Client(requests.Session):
return stream and self._stream_helper(response) \
or self._result(response)
def remove_container(self, container, v=False, link=False):
def remove_container(self, container, v=False, link=False, force=False):
if isinstance(container, dict):
container = container.get('Id')
params = {'v': v, 'link': link}
params = {'v': v, 'link': link, 'force': force}
res = self._delete(self._url("/containers/" + container),
params=params)
self._raise_for_status(res)
def remove_image(self, image):
res = self._delete(self._url("/images/" + image))
def remove_image(self, image, force=False, noprune=False):
params = {'force': force, 'noprune': noprune}
res = self._delete(self._url("/images/" + image), params=params)
self._raise_for_status(res)
def restart(self, container, timeout=10):
@@ -678,8 +746,9 @@ class Client(requests.Session):
params={'term': term}),
True)
def start(self, container, binds=None, volumes_from=None, port_bindings=None,
lxc_conf=None, publish_all_ports=False, links=None, privileged=False):
def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
publish_all_ports=False, links=None, privileged=False,
dns=None, dns_search=None, volumes_from=None, network_mode=None):
if isinstance(container, dict):
container = container.get('Id')
@@ -693,19 +762,7 @@ class Client(requests.Session):
'LxcConf': lxc_conf
}
if binds:
bind_pairs = [
'%s:%s:%s' % (
h, d['bind'],
'ro' if 'ro' in d and d['ro'] else 'rw'
) for h, d in binds.items()
]
start_config['Binds'] = bind_pairs
if volumes_from and not isinstance(volumes_from, six.string_types):
volumes_from = ','.join(volumes_from)
start_config['VolumesFrom'] = volumes_from
start_config['Binds'] = utils.convert_volume_binds(binds)
if port_bindings:
start_config['PortBindings'] = utils.convert_port_bindings(
@@ -726,10 +783,44 @@ class Client(requests.Session):
start_config['Privileged'] = privileged
if utils.compare_version('1.10', self._version) >= 0:
if dns is not None:
start_config['Dns'] = dns
if volumes_from is not None:
if isinstance(volumes_from, six.string_types):
volumes_from = volumes_from.split(',')
start_config['VolumesFrom'] = volumes_from
else:
warning_message = ('{0!r} parameter is discarded. It is only'
' available for API version greater or equal'
' than 1.10')
if dns is not None:
warnings.warn(warning_message.format('dns'),
DeprecationWarning)
if volumes_from is not None:
warnings.warn(warning_message.format('volumes_from'),
DeprecationWarning)
if dns_search:
start_config['DnsSearch'] = dns_search
if network_mode:
start_config['NetworkMode'] = network_mode
url = self._url("/containers/{0}/start".format(container))
res = self._post_json(url, data=start_config)
self._raise_for_status(res)
def resize(self, container, height, width):
if isinstance(container, dict):
container = container.get('Id')
params = {'h': height, 'w': width}
url = self._url("/containers/{0}/resize".format(container))
res = self._post(url, params=params)
self._raise_for_status(res)
def stop(self, container, timeout=10):
if isinstance(container, dict):
container = container.get('Id')

View File

@@ -59,3 +59,7 @@ class InvalidRepository(DockerException):
class InvalidConfigFile(DockerException):
pass
class DeprecatedMethod(DockerException):
pass

View File

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

View File

@@ -92,6 +92,13 @@ def _convert_port_binding(binding):
result['HostIp'] = binding[0]
else:
result['HostPort'] = binding[0]
elif isinstance(binding, dict):
if 'HostPort' in binding:
result['HostPort'] = binding['HostPort']
if 'HostIp' in binding:
result['HostIp'] = binding['HostIp']
else:
raise ValueError(binding)
else:
result['HostPort'] = binding
@@ -116,13 +123,25 @@ def convert_port_bindings(port_bindings):
return result
def convert_volume_binds(binds):
result = []
for k, v in binds.items():
if isinstance(v, dict):
result.append('%s:%s:%s' % (
k, v['bind'], 'ro' if v.get('ro', False) else 'rw'
))
else:
result.append('%s:%s:rw' % (k, v))
return result
def parse_repository_tag(repo):
column_index = repo.rfind(':')
if column_index < 0:
return repo, ""
return repo, None
tag = repo[column_index+1:]
slash_index = tag.find('/')
if slash_index < 0:
return repo[:column_index], tag
return repo, ""
return repo, None

View File

@@ -0,0 +1 @@
version = "0.3.2"

View File

@@ -2,6 +2,8 @@ from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from .service import Service
from .container import Container
from .packages.docker.errors import APIError
log = logging.getLogger(__name__)
@@ -18,11 +20,13 @@ def sort_service_dicts(services):
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', []))]
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'])
@@ -50,21 +54,10 @@ 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 link in service_dict.get('links', []):
if ':' in link:
service_name, link_name = link.split(':', 1)
else:
service_name, link_name = link, None
try:
links.append((project.get_service(service_name), link_name))
except NoSuchService:
raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
links = project.get_links(service_dict)
volumes_from = project.get_volumes_from(service_dict)
del service_dict['links']
project.services.append(Service(client=client, project=name, links=links, **service_dict))
project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict))
return project
@classmethod
@@ -88,22 +81,66 @@ 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]
if include_links:
services = reduce(self._inject_links, services, [])
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):
@@ -124,14 +161,18 @@ class Project(object):
else:
log.info('%s uses an image, skipping' % service.name)
def up(self, service_names=None):
new_containers = []
def up(self, service_names=None, start_links=True, recreate=True):
running_containers = []
for service in self.get_services(service_names):
for (_, new) in service.recreate_containers():
new_containers.append(new)
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 new_containers
return running_containers
def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
@@ -144,6 +185,20 @@ class Project(object):
l.append(container)
return l
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):
def __init__(self, name):

View File

@@ -11,7 +11,7 @@ from .progress_stream import stream_output, StreamOutputError
log = logging.getLogger(__name__)
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged']
DOCKER_CONFIG_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',
@@ -19,8 +19,11 @@ DOCKER_CONFIG_HINTS = {
'priviliged': 'privileged',
'privilige' : 'privileged',
'volume' : 'volumes',
'workdir' : 'working_dir',
}
VALID_NAME_CHARS = '[a-zA-Z0-9]'
class BuildError(Exception):
def __init__(self, service, reason):
@@ -37,11 +40,11 @@ class ConfigError(ValueError):
class Service(object):
def __init__(self, name, client=None, project='default', links=[], **options):
if not re.match('^[a-zA-Z0-9]+$', name):
raise ConfigError('Invalid name: %s' % name)
if not re.match('^[a-zA-Z0-9]+$', project):
raise ConfigError('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 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)
@@ -58,6 +61,7 @@ class Service(object):
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):
@@ -73,9 +77,7 @@ class Service(object):
def start(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
log.info("Starting %s..." % c.name)
self.start_container(c, **options)
self.start_container_if_stopped(c, **options)
def stop(self, **options):
for c in self.containers():
@@ -175,13 +177,19 @@ class Service(object):
return tuples
def recreate_container(self, container, **override_options):
if container.is_running:
container.stop(timeout=1)
try:
container.stop()
except APIError as e:
if (e.response.status_code == 500
and e.explanation
and 'no such process' in str(e.explanation)):
pass
else:
raise
intermediate_container = Container.create(
self.client,
image=container.image,
volumes_from=container.id,
entrypoint=['echo'],
command=[],
)
@@ -190,15 +198,21 @@ class Service(object):
container.remove()
options = dict(override_options)
options['volumes_from'] = intermediate_container.id
new_container = self.create_container(**options)
self.start_container(new_container, volumes_from=intermediate_container.id)
self.start_container(new_container, intermediate_container=intermediate_container)
intermediate_container.remove()
return (intermediate_container, new_container)
def start_container(self, container=None, volumes_from=None, **override_options):
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):
if container is None:
container = self.create_container(**override_options)
@@ -209,12 +223,7 @@ class Service(object):
if options.get('ports', None) is not None:
for port in options['ports']:
port = str(port)
if ':' in port:
external_port, internal_port = port.split(':', 1)
else:
external_port, internal_port = (None, port)
internal_port, external_port = split_port(port)
port_bindings[internal_port] = external_port
volume_bindings = {}
@@ -229,16 +238,31 @@ class Service(object):
}
privileged = options.get('privileged', False)
net = options.get('net', 'bridge')
container.start(
links=self._get_links(link_to_self=override_options.get('one_off', False)),
port_bindings=port_bindings,
binds=volume_bindings,
volumes_from=volumes_from,
volumes_from=self._get_volumes_from(intermediate_container),
privileged=privileged,
network_mode=net,
)
return container
def start_or_create_containers(self):
containers = self.containers(stopped=True)
if len(containers) == 0:
log.info("Creating %s..." % self.next_container_name())
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, one_off=False):
bits = [self.project, self.name]
if one_off:
@@ -267,12 +291,37 @@ class Service(object):
links.append((container.name, container.name_without_project))
return links
def _get_volumes_from(self, intermediate_container=None):
volumes_from = []
for v in self.volumes_from:
if isinstance(v, Service):
for container in v.containers(stopped=True):
volumes_from.append(container.id)
elif isinstance(v, Container):
volumes_from.append(v.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)
# 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', [])
@@ -288,6 +337,11 @@ class Service(object):
if 'volumes' in container_options:
container_options['volumes'] = dict((split_volume(v)[1], {}) for v in container_options['volumes'])
if 'environment' in container_options:
if isinstance(container_options['environment'], list):
container_options['environment'] = dict(split_env(e) for e in container_options['environment'])
container_options['environment'] = dict(resolve_env(k,v) for k,v in container_options['environment'].iteritems())
if self.can_be_built():
if len(self.client.images(name=self._build_tag_name())) == 0:
self.build()
@@ -297,6 +351,10 @@ class Service(object):
if 'privileged' in container_options:
del container_options['privileged']
# net is only required for starting containers, not for creating them
if 'net' in container_options:
del container_options['net']
return container_options
def build(self):
@@ -383,3 +441,34 @@ def split_volume(v):
return v.split(':', 1)
else:
return (None, v)
def split_port(port):
port = str(port)
external_ip = None
if ':' in port:
external_port, internal_port = port.rsplit(':', 1)
if ':' in external_port:
external_ip, external_port = external_port.split(':', 1)
else:
external_port, internal_port = (None, port)
if external_ip:
if external_port:
external_port = (external_ip, external_port)
else:
external_port = (external_ip,)
return internal_port, external_port
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

@@ -3,3 +3,4 @@ PyYAML==3.10
requests==2.2.1
texttable==0.8.1
websocket-client==0.11.0
dockerpty==0.2.1

View File

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

View File

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

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

@@ -1,17 +1,20 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from .testcases import DockerClientTestCase
from mock import patch
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
import sys
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.command.project.kill()
self.command.project.remove_stopped()
@@ -19,7 +22,7 @@ class CLITestCase(DockerClientTestCase):
def test_ps(self, mock_stdout):
self.command.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_ps_default_figfile(self, mock_stdout):
@@ -28,9 +31,9 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['ps'], None)
output = mock_stdout.getvalue()
self.assertIn('fig_simple_1', output)
self.assertIn('fig_another_1', output)
self.assertNotIn('fig_yetanother_1', output)
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):
@@ -39,9 +42,127 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
output = mock_stdout.getvalue()
self.assertNotIn('fig_simple_1', output)
self.assertNotIn('fig_another_1', output)
self.assertIn('fig_yetanother_1', output)
self.assertNotIn('multiplefigfiles_simple_1', output)
self.assertNotIn('multiplefigfiles_another_1', output)
self.assertIn('multiplefigfiles_yetanother_1', output)
def test_up(self):
self.command.dispatch(['up', '-d'], None)
service = self.command.project.get_service('simple')
another = self.command.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.command.project.get_service('web')
db = self.command.project.get_service('db')
console = self.command.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.command.project.get_service('web')
db = self.command.project.get_service('db')
console = self.command.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.command.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.command.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('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.command.project.containers()), 0)
@patch('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.command.project.get_service('db')
console = self.command.project.get_service('console')
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(console.containers()), 0)
@patch('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.command.project.get_service('db')
self.assertEqual(len(db.containers()), 0)
@patch('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.command.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('dockerpty.start')
def test_run_without_command(self, __):
self.command.base_dir = 'tests/fixtures/commands-figfile'
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
for c in self.command.project.containers(stopped=True, one_off=True):
c.remove()
self.command.dispatch(['run', 'implicit'], None)
service = self.command.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.command.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'],
)
def test_rm(self):
service = self.command.project.get_service('simple')

View File

@@ -1,9 +1,49 @@
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')
@@ -44,6 +84,21 @@ class ProjectTest(DockerClientTestCase):
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=['/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
@@ -59,6 +114,107 @@ class ProjectTest(DockerClientTestCase):
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(c.id, old_db_id)
self.assertEqual(c.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(c.id, old_db_id)
self.assertEqual(c.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)

View File

@@ -2,8 +2,10 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from fig import Service
from fig.service import CannotBeScaledError
from fig.container import Container
from fig.packages.docker.errors import APIError
from .testcases import DockerClientTestCase
import os
class ServiceTest(DockerClientTestCase):
def test_containers(self):
@@ -96,17 +98,27 @@ class ServiceTest(DockerClientTestCase):
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.inspect()['HostConfig']['VolumesFrom'])
self.assertIn(volume_container_2.id, host_container.inspect()['HostConfig']['VolumesFrom'])
def test_recreate_containers(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['ps'],
command=['ax']
entrypoint=['sleep'],
command=['300']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['ax'])
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)
@@ -122,16 +134,30 @@ class ServiceTest(DockerClientTestCase):
new_container = tuples[0][1]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['echo'])
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['ax'])
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']['/var/db'], 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, lambda: 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'})
@@ -231,6 +257,27 @@ class ServiceTest(DockerClientTestCase):
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',
])
container = service.start_container().inspect()
self.assertEqual(container['NetworkSettings']['Ports'], {
'8000/tcp': [
{
'HostIp': '127.0.0.1',
'HostPort': '8001',
},
],
'9000/tcp': [
{
'HostIp': '0.0.0.0',
'HostPort': '9001',
},
],
})
def test_scale(self):
service = self.create_service('web')
service.scale(1)
@@ -253,3 +300,43 @@ class ServiceTest(DockerClientTestCase):
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().inspect()
self.assertEqual(container['HostConfig']['NetworkMode'], 'none')
def test_network_mode_bridged(self):
service = self.create_service('web', net='bridge')
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['NetworkMode'], 'bridge')
def test_network_mode_host(self):
service = self.create_service('web', net='host')
container = service.start_container().inspect()
self.assertEqual(container['HostConfig']['NetworkMode'], 'host')
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

@@ -3,8 +3,29 @@ from __future__ import absolute_import
from .. import unittest
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
import os
class CLITestCase(unittest.TestCase):
def test_default_project_name(self):
cwd = os.getcwd()
try:
os.chdir('tests/fixtures/simple-figfile')
command = TopLevelCommand()
self.assertEquals('simplefigfile', command.project_name)
finally:
os.chdir(cwd)
def test_project_name_with_explicit_base_dir(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/simple-figfile'
self.assertEquals('simplefigfile', command.project_name)
def test_project_name_with_explicit_project_name(self):
command = TopLevelCommand()
command.explicit_project_name = 'explicit-project-name'
self.assertEquals('explicitprojectname', command.project_name)
def test_yaml_filename_check(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-figfile'

View File

@@ -16,14 +16,14 @@ class ContainerTest(unittest.TestCase):
"Names":["/figtest_db_1"]
}, has_been_inspected=True)
self.assertEqual(container.dictionary, {
"ID": "abc",
"Id": "abc",
"Image":"busybox:latest",
"Name": "/figtest_db_1",
})
def test_environment(self):
container = Container(None, {
'ID': 'abc',
'Id': 'abc',
'Config': {
'Env': [
'FOO=BAR',

View File

@@ -30,12 +30,19 @@ class ProjectTest(unittest.TestCase):
},
{
'name': 'db',
'image': 'busybox:latest'
'image': 'busybox:latest',
'volumes_from': ['volume']
},
{
'name': 'volume',
'image': 'busybox:latest',
'volumes': ['/tmp'],
}
], None)
self.assertEqual(project.services[0].name, 'db')
self.assertEqual(project.services[1].name, 'web')
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', {
@@ -67,3 +74,68 @@ class ProjectTest(unittest.TestCase):
)
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]
)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from .. import unittest
from fig import Service
from fig.service import ConfigError
from fig.service import ConfigError, split_port
class ServiceTest(unittest.TestCase):
def test_name_validations(self):
@@ -27,3 +27,54 @@ class ServiceTest(unittest.TestCase):
def test_config_validation(self):
self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000']))
Service(name='foo', ports=['8000'])
def test_split_port(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"))
internal_port, external_port = split_port("127.0.0.1::2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, ("127.0.0.1",))
internal_port, external_port = split_port("1000:2000")
self.assertEqual(internal_port, "2000")
self.assertEqual(external_port, "1000")
def test_split_domainname_none(self):
service = Service('foo',
hostname = 'name',
)
service.next_container_name = lambda x: 'foo'
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',
)
service.next_container_name = lambda x: 'foo'
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',
)
service.next_container_name = lambda x: 'foo'
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',
)
service.next_container_name = lambda x: 'foo'
opts = service._get_container_create_options({})
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')