Compare commits

..

68 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
Aanand Prasad
8c16961afd Merge pull request #131 from orchardup/ship-0.3.0
Ship 0.3.0
2014-03-03 18:58:00 +00:00
Ben Firshman
6e9983fc6a Ship 0.3.0 2014-03-03 18:51:03 +00:00
Ben Firshman
bf87f897d7 Merge pull request #130 from orchardup/fix-hanging-recreate
Fix hanging recreate
2014-03-03 18:49:36 +00:00
Aanand Prasad
a00ec9d1f8 Fix: image-defined entrypoint not overridden by intermediate container
This was causing recreation to hang.
2014-03-03 18:06:06 +00:00
Aanand Prasad
be1ba818e4 Document link aliases 2014-03-03 18:00:27 +00:00
Ben Firshman
b0ac88fd06 Merge pull request #78 from orchardup/document-docker-version
Document which versions of Docker Fig supports
2014-03-03 17:02:13 +00:00
Aanand Prasad
a68b4e6dde Merge pull request #128 from orchardup/better-error-message-when-service-is-not-dict
Improve error when service is not a dict
2014-03-03 16:40:59 +00:00
Aanand Prasad
348ba0818f Reformat comments in YAML reference for readability 2014-03-03 16:23:52 +00:00
Aanand Prasad
f79e0e588e Reword port warning in YAML reference, remove it from README 2014-03-03 16:22:02 +00:00
Ben Firshman
3e7360c2c6 Improve error when service is not a dict
Fixes #127
2014-03-03 16:21:42 +00:00
Aanand Prasad
e6016bd5b4 Merge pull request #104 from Gazler/documentation/port-strings
Documentation: Include notes on mapping ports
2014-03-03 16:21:31 +00:00
Ben Firshman
343d3bb556 Remove unsupported Docker 0.7.6 from Travis 2014-03-03 15:43:48 +00:00
Ben Firshman
17b9801ec9 Document what version of Docker is supported 2014-03-03 15:43:10 +00:00
Aanand Prasad
e550451c69 Merge pull request #25 from orchardup/ship-binaries
Ship OS X binaries
2014-03-03 15:29:59 +00:00
Ben Firshman
29f7b78deb Add installation instructions for binaries 2014-03-03 15:20:09 +00:00
Ben Firshman
431ce67f85 Add script to build Linux binary 2014-03-03 15:10:02 +00:00
Ben Firshman
ba66c849b5 Use Python base image and run as normal user 2014-03-03 15:10:02 +00:00
Ben Firshman
9550387e39 Add script to build an OS X binary 2014-03-03 15:09:56 +00:00
Gary Rennie
b06d37f885 Documentation: Include notes on mapping ports
When mapping ports as strings there is an issue with the way YAML parses
numbers in the format "xx:yy" where yy is less than 60 - this issue is
now included in the documentation.

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

http://www.pyinstaller.org/ticket/773
2014-02-28 19:16:32 +00:00
Ben Firshman
b789eca421 Update Orchard description 2014-02-28 19:15:39 +00:00
Ben Firshman
53a5dfe26b Update Orchard description 2014-02-28 18:56:54 +00:00
Ben Firshman
3ed9e16558 Merge pull request #114 from orchardup/refactor-errors
Extract error text into errors.py
2014-02-27 19:16:38 +00:00
Aanand Prasad
ff1496a6a5 Indent string literals 2014-02-26 16:34:45 +00:00
Aanand Prasad
d7c714e1c6 Move "Can't find fig.yml" error into errors.py 2014-02-26 15:44:06 +00:00
Aanand Prasad
d7e2a77907 Refactor connection errors
Makes command.py a lot more readable.
2014-02-26 15:31:14 +00:00
Ben Firshman
07fd01ad46 Exit deploy docs script on Fig error 2014-02-24 18:35:06 +00:00
Ben Firshman
496a1d3bfe Update docker-osx version to 0.8.0 2014-02-24 18:33:38 +00:00
Ben Firshman
0860b7ed73 Merge pull request #111 from teozkr/master
Exclude tests from MANIFEST
2014-02-24 11:56:09 +00:00
Teo Klestrup Röijezon
229b59bd6e remove tests from distribution build 2014-02-23 03:37:31 +01:00
Ben Firshman
05e15e27ef Use sys.exit instead of global 2014-02-19 22:42:21 +00:00
Ben Firshman
51131813a3 Fix broken manifest 2014-02-19 08:52:44 +00:00
Ben Firshman
1327e293f6 Add Dockerfile filename to wordpress guide 2014-02-18 10:56:21 +00:00
Ben Firshman
2edc372f41 Update Wordpress guide so you can edit code 2014-02-18 10:52:24 +00:00
Ben Firshman
d368e2fca9 Ship 0.2.2 2014-02-17 21:37:31 +00:00
Ben Firshman
b9c8e3e057 Fix scale not binding ports 2014-02-17 21:33:05 +00:00
Ben Firshman
5d4210ceb3 Fix tests when there is an image with int tag 2014-02-17 21:32:48 +00:00
Ben Firshman
4a6897ef3b Merge pull request #91 from barnybug/master
Implement topological sort using Cormen/Tarjan algorithm to handle a->b->c dependencies and detect a->b->c->a cycles.
2014-02-12 19:39:08 +00:00
Aanand Prasad
fbff8983e4 Merge pull request #83 from dustinlacewell/print-logs-during-attach
Tell fig up to print logs before attaching
2014-02-12 11:35:24 -08:00
Barnaby Gray
6431d52a2e Implement topological sort using Cormen/Tarjan algorithm to handle a->b->c dependencies and detect a->b->c->a cycles. 2014-02-12 09:09:55 +00:00
Aanand Prasad
7faba11245 Merge pull request #87 from orchardup/stop-projects-in-reverse-order-to-starting
Stop projects in reverse order to starting
2014-02-09 16:22:05 -08:00
Ben Firshman
4723345473 Stop projects in reverse order to starting 2014-02-09 16:01:13 -08:00
Ben Firshman
b310516ba7 Merge pull request #86 from orchardup/update-to-docker-0.8.0
Test Docker 0.8.0 on Travis
2014-02-08 03:41:03 +00:00
Ben Firshman
049a10136c Test Docker 0.8.0 on Travis 2014-02-07 10:35:18 -08:00
Dustin Lacewell
511a9beede Tell fig up to print logs before attaching 2014-02-05 17:19:18 -08:00
Ben Firshman
a8bc51b229 Merge pull request #80 from orchardup/ship-0.2.1
Ship 0.2.1
2014-02-04 18:17:26 -08:00
Aanand Prasad
6bad9484be Ship 0.2.1 2014-02-04 18:14:19 -08:00
Ben Firshman
d52f73b29a Merge pull request #79 from orchardup/strict-config
Throw an error if you specify an unrecognised option in `fig.yml`
2014-02-04 18:10:42 -08:00
Aanand Prasad
edf8f14ac0 Throw an error if you specify an unrecognised option in fig.yml
Closes #27.
2014-02-04 17:46:04 -08:00
Ben Firshman
9faa7e47ed Lock version of docker-osx on home page 2014-02-04 17:15:14 -08:00
Ben Firshman
f92317c5ee Merge pull request #77 from orchardup/friendlier-connection-error
Friendlier connection error for docker-osx users
2014-02-04 17:04:21 -08:00
Ben Firshman
fea3bc2eb1 Install docker-osx from tag 2014-02-04 15:47:12 -08:00
Aanand Prasad
2b89494405 Fix Ubuntu check - forgot to actually inspect the distro 2014-02-04 15:31:05 -08:00
Aanand Prasad
2bac1c10b0 Show installation instructions if it looks like Docker isn't installed 2014-02-04 15:19:50 -08:00
Aanand Prasad
5126649de4 Friendlier connection error for docker-osx users 2014-02-04 14:42:55 -08:00
Ben Firshman
690a7d6dd7 Add /build to gitignore 2014-02-03 16:01:30 -08:00
Ben Firshman
19c0c9bf06 Specify working docker-osx version in docs 2014-02-03 16:00:01 -08:00
42 changed files with 860 additions and 134 deletions

2
.gitignore vendored
View File

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

View File

@@ -4,6 +4,9 @@ python:
- '2.7'
- '3.2'
- '3.3'
env:
- DOCKER_VERSION=0.8.0
- DOCKER_VERSION=0.8.1
matrix:
allow_failures:
- python: '3.2'

View File

@@ -1,6 +1,34 @@
Change log
==========
0.3.0 (2014-03-03)
------------------
- We now ship binaries for OS X and Linux. No more having to install with Pip!
- Add `-f` flag to specify alternate `fig.yml` files
- Add support for custom link names
- Fix a bug where recreating would sometimes hang
- Update docker-py to support Docker 0.8.0.
- Various documentation improvements
- Various error message improvements
Thanks @marksteve, @Gazler and @teozkr!
0.2.2 (2014-02-17)
------------------
- Resolve dependencies using Cormen/Tarjan topological sort
- Fix `fig up` not printing log output
- Stop containers in reverse order to starting
- Fix scale command not binding ports
Thanks to @barnybug and @dustinlacewell for their work on this release.
0.2.1 (2014-02-04)
------------------
- General improvements to error reporting (#77, #79)
0.2.0 (2014-01-31)
------------------

View File

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

View File

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

View File

@@ -22,7 +22,8 @@ web:
links:
- db
ports:
- 8000:8000
- "8000:8000"
- "49100:22"
db:
image: orchardup/postgresql
```
@@ -40,7 +41,7 @@ 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). [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), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
Installation and documentation
------------------------------
@@ -52,4 +53,10 @@ Running the test suite
$ script/test
Building OS X binaries
---------------------
$ script/build-osx
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).

3
bin/fig Executable file
View File

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

View File

@@ -18,7 +18,7 @@ Let's set up the three files that'll get us started. First, our app is going to
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/).
That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Second, we define our Python dependencies in a file called `requirements.txt`:
@@ -29,14 +29,14 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`.
db:
image: orchardup/postgresql
ports:
- 5432
- "5432"
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- 8000:8000
- "8000:8000"
links:
- db

View File

@@ -1,7 +1,7 @@
jekyll:
build: .
ports:
- 4000:4000
- "4000:4000"
volumes:
- .:/code
environment:

View File

@@ -21,7 +21,7 @@ web:
links:
- db
ports:
- 8000:8000
- "8000:8000"
db:
image: orchardup/postgresql
```
@@ -39,7 +39,7 @@ 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). [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), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
Quick start
@@ -49,7 +49,7 @@ Let's get a basic Python web app running on Fig. It assumes a little knowledge o
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
$ 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
@@ -99,7 +99,7 @@ Next, we want to create a Docker image containing all of our app's dependencies.
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/).
This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
We then define a set of services using `fig.yml`:
@@ -107,7 +107,7 @@ We then define a set of services using `fig.yml`:
build: .
command: python app.py
ports:
- 5000:5000
- "5000:5000"
volumes:
- .:/code
links:

View File

@@ -6,18 +6,26 @@ 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):
First, install Docker (version 0.8 or higher). If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
$ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx
$ chmod +x /usr/local/bin/docker-osx
$ docker-osx shell
Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation.
Next, install Fig:
Next, install Fig. On OS X:
$ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/darwin > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
On 64-bit Linux:
$ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/linux > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
$ sudo pip install -U fig
(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.

View File

@@ -18,7 +18,7 @@ Let's set up the three files that'll get us started. First, our app is going to
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/).
That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
@@ -30,14 +30,14 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap
db:
image: orchardup/postgresql
ports:
- 5432
- "5432"
web:
build: .
command: bundle exec rackup -p 3000
volumes:
- .:/myapp
ports:
- 3000:3000
- "3000:3000"
links:
- db

View File

@@ -6,39 +6,40 @@ 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:
Fig makes it nice and easy to run Wordpress in an isolated environment. [Install Fig](install.html), then download Wordpress into the current directory:
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
This will create a directory called `wordpress`, which you can rename to the name of your project if you wish. Inside that directory, we create `Dockerfile`, a file that defines what environment your app is going to run in:
```
FROM orchardup/php5
ADD 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
ADD . /code
```
This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/builder/).
This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
Next up, `fig.yml` starts our web service and a separate MySQL instance:
```
web:
build: .
command: php -S 0.0.0.0:8000 -t /wordpress
command: php -S 0.0.0.0:8000 -t /code
ports:
- 8000:8000
- "8000:8000"
links:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
ports:
- 3306:3306
- "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:
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
```
<?php
@@ -89,4 +90,4 @@ if(file_exists($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.
With those four files in place, run `fig up` inside your Wordpress directory and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress and set it up by visiting [localhost:8000](http://localhost:8000) - or [localdocker:8000](http://localdocker:8000) if you're using docker-osx.

View File

@@ -11,26 +11,38 @@ 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.
-- 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.
-- 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").
-- 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).
-- 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
- "3000"
- "8000:8000"
- "49100:22"
-- Map volumes from the host machine (HOST:CONTAINER).
volumes:

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import signal
from inspect import getdoc
from .. import __version__
from ..project import NoSuchService, DependencyError
from ..project import NoSuchService, ConfigurationError
from ..service import CannotBeScaledError
from .command import Command
from .formatter import Formatter
@@ -39,18 +39,18 @@ def main():
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except (UserError, NoSuchService, DependencyError) as e:
sys.exit(1)
except (UserError, NoSuchService, ConfigurationError) as e:
log.error(e.msg)
exit(1)
sys.exit(1)
except NoSuchCommand as e:
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
exit(1)
sys.exit(1)
except APIError as e:
log.error(e.explanation)
exit(1)
sys.exit(1)
# stolen from docopt master
@@ -70,6 +70,7 @@ class TopLevelCommand(Command):
Options:
--verbose Show more output
--version Print version and exit
-f, --file FILE Specify an alternate fig file (default: fig.yml)
Commands:
build Build or rebuild services
@@ -170,6 +171,9 @@ class TopLevelCommand(Command):
Remove stopped service containers.
Usage: rm [SERVICE...]
Options:
-v Remove volumes associated with containers
"""
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
stopped_containers = [c for c in all_containers if not c.is_running]
@@ -177,7 +181,8 @@ class TopLevelCommand(Command):
if len(stopped_containers) > 0:
print("Going to remove", list_containers(stopped_containers))
if yesno("Are you sure? [yN] ", default=False):
self.project.remove_stopped(service_names=options['SERVICE'])
self.project.remove_stopped(service_names=options['SERVICE'],
remove_volumes=options['-v'])
else:
print("No stopped containers")
@@ -289,7 +294,7 @@ class TopLevelCommand(Command):
if not detached:
to_attach = [c for (s, c) in new]
print("Attaching to", list_containers(to_attach))
log_printer = LogPrinter(to_attach)
log_printer = LogPrinter(to_attach, attach_params={"logs": True})
for (service, container) in new:
service.start_container(container)

View File

@@ -115,7 +115,7 @@ if __name__ == '__main__':
if len(sys.argv) != 2:
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
exit(1)
sys.exit(1)
url = sys.argv[1]
socket = websocket.create_connection(url)

View File

@@ -4,6 +4,8 @@ from __future__ import division
import datetime
import os
import socket
import subprocess
import platform
from .errors import UserError
@@ -108,3 +110,19 @@ def split_buffer(reader, separator):
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

@@ -111,8 +111,9 @@ class Container(object):
def kill(self):
return self.client.kill(self.id)
def remove(self):
return self.client.remove_container(self.id)
def remove(self, **options):
v = options.get('remove_volumes', False)
return self.client.remove_container(self.id, v=v)
def inspect_if_not_inspected(self):
if not self.has_been_inspected:

View File

@@ -17,7 +17,7 @@ import fileinput
import json
import os
import six
from fig.packages import six
from ..utils import utils

View File

@@ -19,7 +19,7 @@ import struct
import requests
import requests.exceptions
import six
from fig.packages import six
from .auth import auth
from .unixconn import unixconn

View File

@@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from fig.packages import six
if six.PY3:
import http.client as httplib

View File

@@ -17,7 +17,7 @@ import tarfile
import tempfile
import requests
import six
from fig.packages import six
def mkbuildcontext(dockerfile):

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

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

View File

@@ -7,31 +7,32 @@ log = logging.getLogger(__name__)
def sort_service_dicts(services):
# 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')]
# Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:]
temporary_marked = set()
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
get_service_names = lambda links: [link.split(':')[0] for link in links]
def visit(n):
if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])):
raise DependencyError('A service can not link to itself: %s' % n['name'])
else:
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
if n in unmarked:
temporary_marked.add(n['name'])
dependents = [m for m in services if n['name'] in get_service_names(m.get('links', []))]
for m in dependents:
visit(m)
temporary_marked.remove(n['name'])
unmarked.remove(n)
sorted_services.insert(0, n)
while unmarked:
visit(unmarked[-1])
return sorted_services
class Project(object):
"""
@@ -52,8 +53,12 @@ class Project(object):
# Reference links by object
links = []
if 'links' in service_dict:
for service_name in service_dict.get('links', []):
links.append(project.get_service(service_name))
for link in service_dict.get('links', []):
if ':' in link:
service_name, link_name = link.split(':', 1)
else:
service_name, link_name = link, None
links.append((project.get_service(service_name), link_name))
del service_dict['links']
project.services.append(Service(client=client, project=name, links=links, **service_dict))
return project
@@ -62,6 +67,8 @@ class Project(object):
def from_config(cls, name, config, client):
dicts = []
for service_name, service in list(config.items()):
if not isinstance(service, dict):
raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.')
service['name'] = service_name
dicts.append(service)
return cls.from_dicts(name, dicts, client)
@@ -116,11 +123,11 @@ class Project(object):
service.start(**options)
def stop(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.stop(**options)
def kill(self, service_names=None, **options):
for service in self.get_services(service_names):
for service in reversed(self.get_services(service_names)):
service.kill(**options)
def build(self, service_names=None, **options):
@@ -151,9 +158,13 @@ class NoSuchService(Exception):
return self.msg
class DependencyError(Exception):
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
return self.msg
class DependencyError(ConfigurationError):
pass

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
@@ -90,7 +111,7 @@ class Service(object):
while len(running_containers) < desired_num:
c = stopped_containers.pop(0)
log.info("Starting %s..." % c.name)
c.start()
self.start_container(c)
running_containers.append(c)
@@ -144,9 +165,9 @@ class Service(object):
intermediate_container = Container.create(
self.client,
image=container.image,
command='echo',
volumes_from=container.id,
entrypoint=None
entrypoint=['echo'],
command=[],
)
intermediate_container.start()
intermediate_container.wait()
@@ -208,8 +229,10 @@ class Service(object):
def _get_links(self):
links = []
for service in self.links:
for service, link_name in self.links:
for container in service.containers():
if link_name:
links.append((container.name, link_name))
links.append((container.name, container.name))
links.append((container.name, container.name_without_project))
for container in self.containers():
@@ -218,8 +241,7 @@ class Service(object):
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
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)

View File

@@ -1,2 +1,3 @@
mock==1.0.1
nose==1.3.0
pyinstaller==2.1

View File

@@ -1,6 +1,5 @@
docopt==0.6.1
PyYAML==3.10
requests==2.2.1
six>=1.3.0
texttable==0.8.1
websocket-client==0.11.0

View File

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

3
script/build-linux Executable file
View File

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

6
script/build-osx Executable file
View File

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

View File

@@ -1,9 +1,8 @@
#!/bin/bash
set -ex
script/build-docs
set -ex
pushd docs/_site
export GIT_DIR=.git-gh-pages

View File

@@ -7,7 +7,7 @@ sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.
sudo apt-get update
echo exit 101 | sudo tee /usr/sbin/policy-rc.d
sudo chmod +x /usr/sbin/policy-rc.d
sudo apt-get install -qy slirp lxc lxc-docker-0.7.5
sudo apt-get install -qy slirp lxc lxc-docker-$DOCKER_VERSION
git clone git://github.com/jpetazzo/sekexe
python setup.py install
pip install -r requirements-dev.txt

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import
from .testcases import DockerClientTestCase
from mock import patch
from six import StringIO
from fig.packages.six import StringIO
from fig.cli.main import TopLevelCommand
class CLITestCase(DockerClientTestCase):
@@ -31,6 +31,28 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_default_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['up', '-d'], None)
self.command.dispatch(['ps'], None)
output = mock_stdout.getvalue()
self.assertIn('fig_simple_1', output)
self.assertIn('fig_another_1', output)
self.assertNotIn('fig_yetanother_1', output)
@patch('sys.stdout', new_callable=StringIO)
def test_alternate_figfile(self, mock_stdout):
self.command.base_dir = 'tests/fixtures/multiple-figfiles'
self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
output = mock_stdout.getvalue()
self.assertNotIn('fig_simple_1', output)
self.assertNotIn('fig_another_1', output)
self.assertIn('fig_yetanother_1', output)
def test_scale(self):
project = self.command.project

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
from __future__ import unicode_literals
from fig.project import Project
from fig.project import Project, ConfigurationError
from .testcases import DockerClientTestCase
@@ -37,6 +37,27 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(project.services[0].name, 'db')
self.assertEqual(project.services[1].name, 'web')
def test_from_config(self):
project = Project.from_config('figtest', {
'web': {
'image': 'ubuntu',
},
'db': {
'image': 'ubuntu',
},
}, self.client)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'ubuntu')
self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
def test_from_config_throws_error_when_not_dict(self):
with self.assertRaises(ConfigurationError):
project = Project.from_config('figtest', {
'web': 'ubuntu',
}, self.client)
def test_get_service(self):
web = self.create_service('web')
project = Project('test', [web], self.client)

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,16 @@ 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'], entrypoint=['ps'])
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['ps'],
command=['ax']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['ax'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
@@ -127,9 +138,10 @@ class ServiceTest(DockerClientTestCase):
new_container = new[0]
intermediate_container = intermediate[0]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None)
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.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.name, 'figtest_db_1')
service.start_container(new_container)
@@ -150,12 +162,19 @@ class ServiceTest(DockerClientTestCase):
def test_start_container_creates_links(self):
db = self.create_service('db')
web = self.create_service('web', links=[db])
web = self.create_service('web', links=[(db, None)])
db.start_container()
web.start_container()
self.assertIn('figtest_db_1', web.containers()[0].links())
self.assertIn('db_1', web.containers()[0].links())
def test_start_container_creates_links_with_names(self):
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'custom_link_name')])
db.start_container()
web.start_container()
self.assertIn('custom_link_name', web.containers()[0].links())
def test_start_container_creates_links_to_its_own_service(self):
db1 = self.create_service('db')
db2 = self.create_service('db')
@@ -225,5 +244,12 @@ class ServiceTest(DockerClientTestCase):
service = self.create_service('web', ports=['8000:8000'])
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
def test_scale_sets_ports(self):
service = self.create_service('web', ports=['8000'])
service.scale(2)
containers = service.containers()
self.assertEqual(len(containers), 2)
for container in containers:
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])

View File

@@ -44,6 +44,27 @@ class SortServiceTest(unittest.TestCase):
self.assertEqual(sorted_services[1]['name'], 'postgres')
self.assertEqual(sorted_services[2]['name'], 'web')
def test_sort_service_dicts_3(self):
services = [
{
'name': 'child'
},
{
'name': 'parent',
'links': ['child']
},
{
'links': ['parent'],
'name': 'grandparent'
},
]
sorted_services = sort_service_dicts(services)
self.assertEqual(len(sorted_services), 3)
self.assertEqual(sorted_services[0]['name'], 'child')
self.assertEqual(sorted_services[1]['name'], 'parent')
self.assertEqual(sorted_services[2]['name'], 'grandparent')
def test_sort_service_dicts_circular_imports(self):
services = [
{
@@ -87,6 +108,30 @@ class SortServiceTest(unittest.TestCase):
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_circular_imports_3(self):
services = [
{
'links': ['b'],
'name': 'a'
},
{
'name': 'b',
'links': ['c']
},
{
'name': 'c',
'links': ['a']
}
]
try:
sort_service_dicts(services)
except DependencyError as e:
self.assertIn('a', e.msg)
self.assertIn('b', e.msg)
else:
self.fail('Should have thrown an DependencyError')
def test_sort_service_dicts_self_imports(self):
services = [
{

View File

@@ -18,16 +18,17 @@ class DockerClientTestCase(unittest.TestCase):
self.client.kill(c['Id'])
self.client.remove_container(c['Id'])
for i in self.client.images():
if 'figtest' in i['Tag']:
if isinstance(i['Tag'], basestring) and 'figtest' in i['Tag']:
self.client.remove_image(i)
def create_service(self, name, **kwargs):
if 'command' not in kwargs:
kwargs['command'] = ["/bin/sleep", "300"]
return Service(
project='figtest',
name=name,
client=self.client,
image="ubuntu",
command=["/bin/sleep", "300"],
**kwargs
)