Compare commits

...

102 Commits
0.1.1 ... 0.2.1

Author SHA1 Message Date
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
48 changed files with 1537 additions and 453 deletions

4
.gitignore vendored
View File

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

View File

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

270
README.md
View File

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

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

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

10
docs/Dockerfile Normal file
View File

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

3
docs/Gemfile Normal file
View File

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

62
docs/Gemfile.lock Normal file
View File

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

1
docs/_config.yml Normal file
View File

@@ -0,0 +1 @@
markdown: redcarpet

View File

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

81
docs/cli.md Normal file
View File

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

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

File diff suppressed because one or more lines are too long

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

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

93
docs/django.md Normal file
View File

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

31
docs/env.md Normal file
View File

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

8
docs/fig.yml Normal file
View File

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

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

157
docs/index.md Normal file
View File

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

23
docs/install.md Normal file
View File

@@ -0,0 +1,23 @@
---
layout: default
title: Installing Fig
---
Installing Fig
==============
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/0.7.6/docker-osx > /usr/local/bin/docker-osx
$ chmod +x /usr/local/bin/docker-osx
$ docker-osx shell
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
Next, install Fig:
$ sudo pip install -U fig
(This command also upgrades Fig when we release a new version. If you dont have pip installed, try `brew install python` or `apt-get install python-pip`.)
That should be all you need! Run `fig --version` to see if it worked.

99
docs/rails.md Normal file
View File

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

92
docs/wordpress.md Normal file
View File

@@ -0,0 +1,92 @@
---
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 write a `Dockerfile` which installs PHP and Wordpress:
```
FROM orchardup/php5
ADD http://wordpress.org/wordpress-3.8.1.tar.gz /wordpress.tar.gz
RUN tar -xzf /wordpress.tar.gz
ADD wp-config.php /wordpress/wp-config.php
ADD router.php /router.php
```
This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/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 /wordpress
ports:
- 8000:8000
links:
- db
db:
image: orchardup/mysql
ports:
- 3306:3306
environment:
MYSQL_DATABASE: wordpress
```
Our Dockerfile relies on two supporting files - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
```
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', getenv("DB_1_PORT_3306_TCP_ADDR") . ":" . getenv("DB_1_PORT_3306_TCP_PORT"));
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
```
Finally, `router.php` tells PHP's built-in web server how to run Wordpress:
```
<?php
$root = $_SERVER['DOCUMENT_ROOT'];
chdir($root);
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
set_include_path(get_include_path().':'.__DIR__);
if(file_exists($root.$path))
{
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
$path = rtrim($path,'/').'/index.php';
if(strpos($path,'.php') === false) return false;
else {
chdir(dirname($root.$path));
require_once $root.$path;
}
}else include_once 'index.php';
```
With those four files in place, run `fig up` and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress and set it up by visiting [localhost:8000](http://localhost:8000) - or [localdocker:8000](http://localdocker:8000) if you're using docker-osx.

43
docs/yml.md Normal file
View File

@@ -0,0 +1,43 @@
---
layout: default
title: fig.yml reference
---
fig.yml reference
=================
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
```yaml
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
build: /path/to/build/dir
-- Override the default command.
command: bundle exec thin -p 3000
-- Link to containers in another service (see "Communicating between containers").
links:
- db
- redis
-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).
ports:
- 3000
- 8000:8000
-- Map volumes from the host machine (HOST:CONTAINER).
volumes:
- cache/:/tmp/cache
-- Add environment variables.
environment:
RACK_ENV: development
```

View File

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

View File

@@ -7,11 +7,13 @@ 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, docker_url
from .utils import cached_property, docker_url, call_silently, is_mac, is_ubuntu
from .errors import UserError
log = logging.getLogger(__name__)
@@ -23,7 +25,29 @@ class Command(DocoptCommand):
try:
super(Command, self).dispatch(*args, **kwargs)
except ConnectionError:
raise UserError("""
if call_silently(['which', 'docker']) != 0:
if is_mac():
raise UserError("""
Couldn't connect to Docker daemon. You might need to install docker-osx:
https://github.com/noplay/docker-osx
""")
elif is_ubuntu():
raise UserError("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/ubuntulinux/
""")
else:
raise UserError("""
Couldn't connect to Docker daemon. You might need to install Docker:
http://docs.docker.io/en/latest/installation/
""")
elif call_silently(['which', 'docker-osx']) == 0:
raise UserError("Couldn't connect to Docker daemon - you might need to run `docker-osx shell`.")
else:
raise UserError("""
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.
@@ -36,8 +60,9 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
@cached_property
def project(self):
try:
yaml_path = os.path.join(self.base_dir, 'fig.yml')
yaml_path = self.check_yaml_filename()
config = yaml.load(open(yaml_path))
except IOError as e:
if e.errno == errno.ENOENT:
log.error("Can't find %s. Are you in the right directory?", os.path.basename(e.filename))
@@ -46,7 +71,10 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
exit(1)
return Project.from_config(self.project_name, config, self.client)
try:
return Project.from_config(self.project_name, config, self.client)
except ConfigError as e:
raise UserError(six.text_type(e))
@cached_property
def project_name(self):
@@ -60,3 +88,12 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ
def formatter(self):
return Formatter()
def check_yaml_filename(self):
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
log.warning("Fig just read the file 'fig.yaml' on startup, rather than 'fig.yml'")
log.warning("Please be aware that fig.yml the expected extension in most cases, and using .yaml can cause compatibility issues in future")
return os.path.join(self.base_dir, 'fig.yaml')
else:
return os.path.join(self.base_dir, 'fig.yml')

View File

@@ -6,6 +6,7 @@ from itertools import cycle
from .multiplexer import Multiplexer
from . import colors
from .utils import split_buffer
class LogPrinter(object):
@@ -31,8 +32,9 @@ class LogPrinter(object):
def _make_log_generator(self, container, color_fn):
prefix = color_fn(container.name + " | ")
for line in split_buffer(self._attach(container), '\n'):
yield prefix + line
# Attach to container before log printer starts running
line_generator = split_buffer(self._attach(container), '\n')
return (prefix + line.decode('utf-8') for line in line_generator)
def _attach(self, container):
params = {
@@ -43,21 +45,3 @@ class LogPrinter(object):
params.update(self.attach_params)
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
return container.attach(**params)
def split_buffer(reader, separator):
"""
Given a generator which yields strings and a separator string,
joins all input, splits on the separator and yields each chunk.
Requires that each input string is decodable as UTF-8.
"""
buffered = ''
for data in reader:
lines = (buffered + data.decode('utf-8')).split(separator)
for line in lines[:-1]:
yield line + separator
if len(lines) > 1:
buffered = lines[-1]
if len(buffered) > 0:
yield buffered

View File

@@ -4,12 +4,11 @@ import logging
import sys
import re
import signal
import sys
from inspect import getdoc
from .. import __version__
from ..project import NoSuchService
from ..project import NoSuchService, DependencyError
from ..service import CannotBeScaledError
from .command import Command
from .formatter import Formatter
@@ -41,10 +40,7 @@ def main():
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except UserError as e:
log.error(e.msg)
exit(1)
except NoSuchService as e:
except (UserError, NoSuchService, DependencyError) as e:
log.error(e.msg)
exit(1)
except NoSuchCommand as e:
@@ -200,12 +196,20 @@ class TopLevelCommand(Command):
Usage: run [options] SERVICE COMMAND [ARGS...]
Options:
-d Detached mode: Run container in the background, print new container name
-d Detached mode: Run container in the background, print new
container name
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
"""
service = self.project.get_service(options['SERVICE'])
tty = True
if options['-d'] or options['-T'] or not sys.stdin.isatty():
tty = False
container_options = {
'command': [options['COMMAND']] + options['ARGS'],
'tty': not options['-d'],
'tty': tty,
'stdin_open': not options['-d'],
}
container = service.create_container(one_off=True, **container_options)
@@ -213,12 +217,7 @@ class TopLevelCommand(Command):
service.start_container(container, ports=None)
print(container.name)
else:
with self._attach_to_container(
container.id,
interactive=True,
logs=True,
raw=True
) as c:
with self._attach_to_container(container.id, raw=tty) as c:
service.start_container(container, ports=None)
c.run()
@@ -310,35 +309,15 @@ class TopLevelCommand(Command):
print("Gracefully stopping... (press Ctrl+C again to force)")
self.project.stop(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,
)
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,
)
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=stdio,
socket_out=stdio,
socket_err=stderr,
socket_in=socket_in,
socket_out=socket_out,
socket_err=socket_err,
raw=raw,
)

View File

@@ -1,7 +1,6 @@
from __future__ import print_function
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
from select import select
import sys
import tty
import fcntl
@@ -57,15 +56,15 @@ class SocketClient:
def run(self):
if self.socket_in is not None:
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
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_ws, args=(self.socket_out, sys.stdout)))
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_ws, args=(self.socket_err, sys.stderr)))
recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_err, sys.stderr)))
for t in recv_threads:
t.join()
@@ -76,10 +75,10 @@ class SocketClient:
thread.start()
return thread
def recv_ws(self, socket, stream):
def recv(self, socket, stream):
try:
while True:
chunk = socket.recv()
chunk = socket.recv(4096)
if chunk:
stream.write(chunk)
@@ -89,24 +88,21 @@ class SocketClient:
except Exception as e:
log.debug(e)
def send_ws(self, socket, stream):
def send(self, socket, stream):
while True:
r, w, e = select([stream.fileno()], [], [])
chunk = stream.read(1)
if r:
chunk = stream.read(1)
if chunk == '':
socket.send_close()
break
else:
try:
socket.send(chunk)
except Exception as e:
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
break
else:
raise e
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:

View File

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

View File

View File

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

View File

@@ -3,7 +3,7 @@ from __future__ import absolute_import
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):
@@ -38,6 +38,10 @@ class Container(object):
def id(self):
return self.dictionary['ID']
@property
def image(self):
return self.dictionary['Image']
@property
def short_id(self):
return self.id[:10]
@@ -46,6 +50,10 @@ class Container(object):
def name(self):
return self.dictionary['Name'][1:]
@property
def name_without_project(self):
return '_'.join(self.dictionary['Name'].split('_')[1:])
@property
def number(self):
try:

View File

@@ -69,9 +69,11 @@ class Client(requests.Session):
timeout=DEFAULT_TIMEOUT_SECONDS):
super(Client, self).__init__()
if base_url is None:
base_url = "unix://var/run/docker.sock"
if base_url.startswith('unix:///'):
base_url = "http+unix://var/run/docker.sock"
if 'unix:///' in base_url:
base_url = base_url.replace('unix:/', 'unix:')
if base_url.startswith('unix:'):
base_url = "http+" + base_url
if base_url.startswith('tcp:'):
base_url = base_url.replace('tcp:', 'http:')
if base_url.endswith('/'):
@@ -81,7 +83,7 @@ class Client(requests.Session):
self._timeout = timeout
self._auth_configs = auth.load_config()
self.mount('unix://', unixconn.UnixAdapter(base_url, timeout))
self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout))
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
@@ -122,7 +124,8 @@ class Client(requests.Session):
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False):
network_disabled=False, entrypoint=None,
cpu_shares=None, working_dir=None):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
@@ -134,15 +137,12 @@ class Client(requests.Session):
exposed_ports = {}
for port_definition in ports:
port = port_definition
proto = None
proto = 'tcp'
if isinstance(port_definition, tuple):
if len(port_definition) == 2:
proto = port_definition[1]
port = port_definition[0]
exposed_ports['{0}{1}'.format(
port,
'/' + proto if proto else ''
)] = {}
exposed_ports['{0}/{1}'.format(port, proto)] = {}
ports = exposed_ports
if volumes and isinstance(volumes, list):
@@ -154,6 +154,7 @@ class Client(requests.Session):
attach_stdin = False
attach_stdout = False
attach_stderr = False
stdin_once = False
if not detach:
attach_stdout = True
@@ -161,6 +162,7 @@ class Client(requests.Session):
if stdin_open:
attach_stdin = True
stdin_once = True
return {
'Hostname': hostname,
@@ -168,6 +170,7 @@ class Client(requests.Session):
'User': user,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,
'Memory': mem_limit,
'AttachStdin': attach_stdin,
'AttachStdout': attach_stdout,
@@ -178,7 +181,10 @@ class Client(requests.Session):
'Image': image,
'Volumes': volumes,
'VolumesFrom': volumes_from,
'NetworkDisabled': network_disabled
'NetworkDisabled': network_disabled,
'Entrypoint': entrypoint,
'CpuShares': cpu_shares,
'WorkingDir': working_dir
}
def _post_json(self, url, data, **kwargs):
@@ -219,7 +225,7 @@ class Client(requests.Session):
def _stream_result(self, response):
"""Generator for straight-out, non chunked-encoded HTTP responses."""
self._raise_for_status(response)
for line in response.iter_lines(chunk_size=1):
for line in response.iter_lines(chunk_size=1, decode_unicode=True):
# filter out keep-alive new lines
if line:
yield line + '\n'
@@ -409,11 +415,13 @@ class Client(requests.Session):
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, name=None):
network_disabled=False, name=None, entrypoint=None,
cpu_shares=None, working_dir=None):
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from, network_disabled
ports, environment, dns, volumes, volumes_from, network_disabled,
entrypoint, cpu_shares, working_dir
)
return self.create_container_from_config(config, name)
@@ -475,27 +483,34 @@ class Client(requests.Session):
return [x['Id'] for x in res]
return res
def import_image(self, src, data=None, repository=None, tag=None):
def import_image(self, src=None, repository=None, tag=None, image=None):
u = self._url("/images/create")
params = {
'repo': repository,
'tag': tag
}
try:
# XXX: this is ways not optimal but the only way
# for now to import tarballs through the API
fic = open(src)
data = fic.read()
fic.close()
src = "-"
except IOError:
# file does not exists or not a file (URL)
data = None
if isinstance(src, six.string_types):
params['fromSrc'] = src
return self._result(self._post(u, data=data, params=params))
return self._result(self._post(u, data=src, params=params))
if src:
try:
# XXX: this is ways not optimal but the only way
# for now to import tarballs through the API
fic = open(src)
data = fic.read()
fic.close()
src = "-"
except IOError:
# file does not exists or not a file (URL)
data = None
if isinstance(src, six.string_types):
params['fromSrc'] = src
return self._result(self._post(u, data=data, params=params))
return self._result(self._post(u, data=src, params=params))
if image:
params['fromImage'] = image
return self._result(self._post(u, data=None, params=params))
raise Exception("Must specify a src or image")
def info(self):
return self._result(self._get(self._url("/info")),
@@ -577,13 +592,13 @@ class Client(requests.Session):
self._raise_for_status(res)
json_ = res.json()
s_port = str(private_port)
f_port = None
if s_port in json_['NetworkSettings']['PortMapping']['Udp']:
f_port = json_['NetworkSettings']['PortMapping']['Udp'][s_port]
elif s_port in json_['NetworkSettings']['PortMapping']['Tcp']:
f_port = json_['NetworkSettings']['PortMapping']['Tcp'][s_port]
h_ports = None
return f_port
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp')
if h_ports is None:
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp')
return h_ports
def pull(self, repository, tag=None, stream=False):
registry, repo_name = auth.resolve_repository_name(repository)
@@ -695,8 +710,11 @@ class Client(requests.Session):
start_config['PublishAllPorts'] = publish_all_ports
if links:
if isinstance(links, dict):
links = six.iteritems(links)
formatted_links = [
'{0}:{1}'.format(k, v) for k, v in sorted(six.iteritems(links))
'{0}:{1}'.format(k, v) for k, v in sorted(links)
]
start_config['Links'] = formatted_links

View File

@@ -36,7 +36,7 @@ class UnixHTTPConnection(httplib.HTTPConnection, object):
def connect(self):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(self.timeout)
sock.connect(self.base_url.replace("unix:/", ""))
sock.connect(self.base_url.replace("http+unix:/", ""))
self.sock = sock
def _extract_path(self, url):

View File

@@ -2,21 +2,36 @@ from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from .service import Service
from .compat.functools import cmp_to_key
log = logging.getLogger(__name__)
def sort_service_dicts(services):
# Sort in dependency order
def cmp(x, y):
x_deps_y = y['name'] in x.get('links', [])
y_deps_x = x['name'] in y.get('links', [])
if x_deps_y and not y_deps_x:
return 1
elif y_deps_x and not x_deps_y:
return -1
return 0
return sorted(services, key=cmp_to_key(cmp))
# Get all services that are dependant on another.
dependent_services = [s for s in services if s.get('links')]
flatten_links = sum([s['links'] for s in dependent_services], [])
# Get all services that are not linked to and don't link to others.
non_dependent_sevices = [s for s in services if s['name'] not in flatten_links and not s.get('links')]
sorted_services = []
# Topological sort.
while dependent_services:
n = dependent_services.pop()
# Check if a service is dependent on itself, if so raise an error.
if n['name'] in n.get('links', []):
raise DependencyError('A service can not link to itself: %s' % n['name'])
sorted_services.append(n)
for l in n['links']:
# Get the linked service.
linked_service = next(s for s in services if l == s['name'])
# Check that there isn't a circular import between services.
if n['name'] in linked_service.get('links', []):
raise DependencyError('Circular import between %s and %s' % (n['name'], linked_service['name']))
# Check the linked service has no links and is not already in the
# sorted service list.
if not linked_service.get('links') and linked_service not in sorted_services:
sorted_services.insert(0, linked_service)
return non_dependent_sevices + sorted_services
class Project(object):
"""
@@ -134,3 +149,11 @@ class NoSuchService(Exception):
def __str__(self):
return self.msg
class DependencyError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg

View File

@@ -10,6 +10,14 @@ from .container import Container
log = logging.getLogger(__name__)
DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
DOCKER_CONFIG_HINTS = {
'link': 'links',
'port': 'ports',
'volume': 'volumes',
}
class BuildError(Exception):
pass
@@ -18,14 +26,27 @@ class CannotBeScaledError(Exception):
pass
class ConfigError(ValueError):
pass
class Service(object):
def __init__(self, name, client=None, project='default', links=[], **options):
if not re.match('^[a-zA-Z0-9]+$', name):
raise ValueError('Invalid name: %s' % name)
raise ConfigError('Invalid name: %s' % name)
if not re.match('^[a-zA-Z0-9]+$', project):
raise ValueError('Invalid project: %s' % project)
raise ConfigError('Invalid project: %s' % project)
if 'image' in options and 'build' in options:
raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
supported_options = DOCKER_CONFIG_KEYS + ['build']
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
@@ -143,9 +164,10 @@ class Service(object):
intermediate_container = Container.create(
self.client,
image='ubuntu',
image=container.image,
command='echo',
volumes_from=container.id,
entrypoint=None
)
intermediate_container.start()
intermediate_container.wait()
@@ -171,9 +193,10 @@ class Service(object):
port = str(port)
if ':' in port:
external_port, internal_port = port.split(':', 1)
port_bindings[int(internal_port)] = int(external_port)
else:
port_bindings[int(port)] = None
external_port, internal_port = (None, port)
port_bindings[internal_port] = external_port
volume_bindings = {}
@@ -205,15 +228,18 @@ class Service(object):
return max(numbers) + 1
def _get_links(self):
links = {}
links = []
for service in self.links:
for container in service.containers():
links[container.name] = container.name
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
for container in self.containers():
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
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)
@@ -224,8 +250,8 @@ class Service(object):
port = str(port)
if ':' in port:
port = port.split(':')[-1]
if '/' not in port:
port = "%s/tcp" % port
if '/' in port:
port = tuple(port.split('/'))
ports.append(port)
container_options['ports'] = ports

View File

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

View File

@@ -1,7 +1,6 @@
requests==1.2.3
websocket-client==0.11.0
docopt==0.6.1
PyYAML==3.10
requests==2.2.1
six>=1.3.0
texttable==0.8.1
# docker requires six==1.3.0
six==1.3.0
websocket-client==0.11.0

5
script/build-docs Executable file
View File

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

31
script/deploy-docs Executable file
View File

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

2
script/open-docs Executable file
View File

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

View File

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

View File

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

View File

@@ -1,18 +1,35 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from . import unittest
from .testcases import DockerClientTestCase
from mock import patch
from six import StringIO
from fig.cli.main import TopLevelCommand
class CLITestCase(unittest.TestCase):
class CLITestCase(DockerClientTestCase):
def setUp(self):
super(CLITestCase, self).setUp()
self.command = TopLevelCommand()
self.command.base_dir = 'tests/fixtures/simple-figfile'
def tearDown(self):
self.command.project.kill()
self.command.project.remove_stopped()
def test_yaml_filename_check(self):
self.command.base_dir = 'tests/fixtures/longer-filename-figfile'
project = self.command.project
self.assertTrue( project.get_service('definedinyamlnotyml'), "Service: definedinyamlnotyml should have been loaded from .yaml file" )
def test_help(self):
self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None))
def test_ps(self):
@patch('sys.stdout', new_callable=StringIO)
def test_ps(self, mock_stdout):
self.command.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
def test_scale(self):
project = self.command.project

View File

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

View File

@@ -61,6 +61,10 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(web.containers(stopped=True)), 1)
self.assertEqual(len(db.containers(stopped=True)), 1)
# remove intermediate containers
for (service, container) in old:
container.remove()
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')

View File

@@ -1,30 +1,34 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from fig import Service
from fig.service import CannotBeScaledError
from fig.service import CannotBeScaledError, ConfigError
from .testcases import DockerClientTestCase
class ServiceTest(DockerClientTestCase):
def test_name_validations(self):
self.assertRaises(ValueError, lambda: Service(name=''))
self.assertRaises(ConfigError, 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__'))
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(ValueError, lambda: Service(name='foo', project='_'))
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_containers(self):
foo = self.create_service('foo')
bar = self.create_service('bar')
@@ -110,9 +114,10 @@ class ServiceTest(DockerClientTestCase):
self.assertIn('/var/db', container.inspect()['Volumes'])
def test_recreate_containers(self):
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'])
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps'])
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Env'], ['FOO=1'])
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
volume_path = old_container.inspect()['Volumes']['/var/db']
@@ -120,12 +125,16 @@ class ServiceTest(DockerClientTestCase):
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
(old, new) = service.recreate_containers()
self.assertEqual(len(old), 1)
(intermediate, new) = service.recreate_containers()
self.assertEqual(len(intermediate), 1)
self.assertEqual(len(new), 1)
new_container = new[0]
self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2'])
intermediate_container = intermediate[0]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None)
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.name, 'figtest_db_1')
service.start_container(new_container)
self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path)
@@ -149,8 +158,14 @@ class ServiceTest(DockerClientTestCase):
db.start_container()
web.start_container()
self.assertIn('figtest_db_1', web.containers()[0].links())
db.stop(timeout=1)
web.stop(timeout=1)
self.assertIn('db_1', web.containers()[0].links())
def test_start_container_creates_links_to_its_own_service(self):
db1 = self.create_service('db')
db2 = self.create_service('db')
db1.start_container()
db2.start_container()
self.assertIn('db_1', db2.containers()[0].links())
def test_start_container_builds_images(self):
service = Service(
@@ -179,9 +194,14 @@ class ServiceTest(DockerClientTestCase):
def test_start_container_creates_ports(self):
service = self.create_service('web', ports=[8000])
container = service.start_container().inspect()
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
self.assertEqual(list(container['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
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['HostConfig']['PortBindings'].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()

103
tests/sort_service_test.py Normal file
View File

@@ -0,0 +1,103 @@
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_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_self_imports(self):
services = [
{
'links': ['web'],
'name': 'web'
},
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('web', e.msg)
else:
self.fail('Should have thrown an DependencyError')

View File

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

View File

@@ -10,13 +10,16 @@ class DockerClientTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.client = Client(docker_url())
cls.client.pull('ubuntu')
cls.client.pull('ubuntu', tag='latest')
def setUp(self):
for c in self.client.containers(all=True):
if c['Names'] and 'figtest' in c['Names'][0]:
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images():
if 'figtest' in i['Tag']:
self.client.remove_image(i)
def create_service(self, name, **kwargs):
return Service(