mirror of
https://github.com/docker/compose.git
synced 2026-02-10 02:29:25 +08:00
Compare commits
210 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53958cf9df | ||
|
|
f94ce7767c | ||
|
|
62607f4f04 | ||
|
|
e4e9f0bc19 | ||
|
|
e0637990aa | ||
|
|
03842f92e8 | ||
|
|
8a2bbeb1eb | ||
|
|
95681246a5 | ||
|
|
80279f71d8 | ||
|
|
ae7573b9b8 | ||
|
|
ee880ca7be | ||
|
|
21837ac132 | ||
|
|
c270c13baa | ||
|
|
cbdeff99ee | ||
|
|
303e0cfd86 | ||
|
|
880bc0d8e3 | ||
|
|
89d2653662 | ||
|
|
8b75f7c7d3 | ||
|
|
9967d706e9 | ||
|
|
d5bc521ab0 | ||
|
|
d4f76ba5f9 | ||
|
|
239da2ef69 | ||
|
|
1d18d747a5 | ||
|
|
2883b5be6b | ||
|
|
e34b433d93 | ||
|
|
b6d0b8468b | ||
|
|
5e5bd939be | ||
|
|
140ccc7d4e | ||
|
|
b4cbcbefa6 | ||
|
|
9049f4e378 | ||
|
|
0e58c05361 | ||
|
|
818516f150 | ||
|
|
2abf25d827 | ||
|
|
837593e1ab | ||
|
|
274728e5fb | ||
|
|
51fb2e6149 | ||
|
|
cc29b21bfc | ||
|
|
00f5c3bb85 | ||
|
|
33795912ff | ||
|
|
92a2deb979 | ||
|
|
40fad23e0b | ||
|
|
6c8472dd67 | ||
|
|
a9838e9116 | ||
|
|
01d0e49a1c | ||
|
|
ddc721ec8a | ||
|
|
5035a10cbe | ||
|
|
fae387168f | ||
|
|
5d71c33cd7 | ||
|
|
9a5a021f91 | ||
|
|
3e7e6e7656 | ||
|
|
1bab14213d | ||
|
|
db396b81ef | ||
|
|
f60621ee1b | ||
|
|
7c9c55785d | ||
|
|
8f8b0bbd16 | ||
|
|
ddf6819a75 | ||
|
|
ee49e7055b | ||
|
|
ac90e0e939 | ||
|
|
ea93c01dfb | ||
|
|
cf18a3141f | ||
|
|
2ebec04811 | ||
|
|
b43b007b92 | ||
|
|
33aada05a4 | ||
|
|
0bdd8637db | ||
|
|
e8472be6d5 | ||
|
|
84667636a2 | ||
|
|
df9f66d437 | ||
|
|
ae67d55bf2 | ||
|
|
18525554ed | ||
|
|
64513e8d6f | ||
|
|
deb7f3c5b6 | ||
|
|
48eb5e5c82 | ||
|
|
65071aafb0 | ||
|
|
4ee87a7029 | ||
|
|
977ec7c941 | ||
|
|
40d04a076c | ||
|
|
4646ac85b0 | ||
|
|
8773bad99a | ||
|
|
084db337a0 | ||
|
|
f47f075f02 | ||
|
|
7abc4fbf3a | ||
|
|
855a9c623c | ||
|
|
7e2d86c510 | ||
|
|
405079f744 | ||
|
|
fc1bbb45b1 | ||
|
|
24a6d1d836 | ||
|
|
f3d273864d | ||
|
|
ce8ef7afe7 | ||
|
|
62bba1684b | ||
|
|
07f3c78369 | ||
|
|
cbfbb9899d | ||
|
|
b428988ef6 | ||
|
|
4c72598e68 | ||
|
|
c6e19e34f7 | ||
|
|
c0dbb1c2ec | ||
|
|
4bec39535f | ||
|
|
7c1ec74cf6 | ||
|
|
8b77b51c15 | ||
|
|
7070e06ac6 | ||
|
|
5b9c228cf8 | ||
|
|
15e8c9ffbb | ||
|
|
caccf96d3f | ||
|
|
720cc192bb | ||
|
|
573fae089f | ||
|
|
83cd16e373 | ||
|
|
74fb400fef | ||
|
|
a07b00606b | ||
|
|
65f23583ae | ||
|
|
c8a58f2547 | ||
|
|
56c6efdfce | ||
|
|
8ed86ed551 | ||
|
|
d2f4c81d62 | ||
|
|
b20190da98 | ||
|
|
5d9a5a0c84 | ||
|
|
bb7613f37b | ||
|
|
9bd54d7be2 | ||
|
|
5c8fac5993 | ||
|
|
3e2fd6a2a1 | ||
|
|
f36dd414a0 | ||
|
|
af1b0ed088 | ||
|
|
feafea2c6d | ||
|
|
c4f5ed839f | ||
|
|
b4c905dc83 | ||
|
|
804e2cdcb1 | ||
|
|
0bb5e48f53 | ||
|
|
21528f08d4 | ||
|
|
b1e7f548f4 | ||
|
|
cdcea98290 | ||
|
|
c06456da37 | ||
|
|
7b31fdf6f6 | ||
|
|
e38b403b14 | ||
|
|
ee0c4bf690 | ||
|
|
8c583d1bb2 | ||
|
|
ea4753c49a | ||
|
|
3956d85a8c | ||
|
|
8a0071d9c1 | ||
|
|
5db6c9f51b | ||
|
|
f5f9357736 | ||
|
|
bdc6b47e1f | ||
|
|
3669236aa1 | ||
|
|
207e83ac2f | ||
|
|
3c5e334d9d | ||
|
|
a8e275a432 | ||
|
|
887a30e327 | ||
|
|
7a1fb3a8d2 | ||
|
|
d4000e07a9 | ||
|
|
a3d024e11d | ||
|
|
b92e998929 | ||
|
|
f448a841c5 | ||
|
|
c9c844c279 | ||
|
|
342f187318 | ||
|
|
d063f0e00c | ||
|
|
0614e2c590 | ||
|
|
431b3dc2b2 | ||
|
|
544cd884ee | ||
|
|
c6efb45585 | ||
|
|
38008a87e8 | ||
|
|
059d240824 | ||
|
|
7a4b69edc0 | ||
|
|
8cab05feb4 | ||
|
|
892677a9d3 | ||
|
|
00a1835fae | ||
|
|
7888027425 | ||
|
|
0760ea1b00 | ||
|
|
31f0907732 | ||
|
|
9bec059cc7 | ||
|
|
f600fa8bf3 | ||
|
|
c6e91db32f | ||
|
|
b101118d1e | ||
|
|
30ea4508c3 | ||
|
|
3c91315426 | ||
|
|
bf8875d930 | ||
|
|
93b9b6fd9f | ||
|
|
f96a1a0b35 | ||
|
|
8de07ccf65 | ||
|
|
ff9fa5661d | ||
|
|
17b9cc430c | ||
|
|
d8a2a0f003 | ||
|
|
490742b892 | ||
|
|
3fa80cd974 | ||
|
|
9ede185d4b | ||
|
|
aaf90639a0 | ||
|
|
5ba7040df2 | ||
|
|
3d411ed0bb | ||
|
|
dd1f8934ad | ||
|
|
febcbcddb9 | ||
|
|
88c74d67f6 | ||
|
|
0fb915e57e | ||
|
|
e0a21e3df4 | ||
|
|
45b7bd4361 | ||
|
|
36002f95ed | ||
|
|
0a92cbfa4d | ||
|
|
5b1fd64708 | ||
|
|
9b289b6f3b | ||
|
|
6d0702e607 | ||
|
|
38478ea504 | ||
|
|
a39db86651 | ||
|
|
853d8ad280 | ||
|
|
17c6ae067a | ||
|
|
770e78fdce | ||
|
|
e5065bed16 | ||
|
|
c0676e3fa3 | ||
|
|
d4f3ed1840 | ||
|
|
9ed6538693 | ||
|
|
ff65a3e1b0 | ||
|
|
ebf9bf387c | ||
|
|
fdc1e0f2e1 | ||
|
|
f3eff9a389 | ||
|
|
2857631e90 | ||
|
|
89cd7d8db0 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
/dist
|
||||
/docs/_site
|
||||
/docs/.git-gh-pages
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,11 +1,23 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
install:
|
||||
- python setup.py install
|
||||
- pip install nose==1.3.0
|
||||
script: nosetests
|
||||
|
||||
- '2.6'
|
||||
- '2.7'
|
||||
- '3.2'
|
||||
- '3.3'
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: '3.2'
|
||||
- python: '3.3'
|
||||
install: script/travis-install
|
||||
script:
|
||||
- pwd
|
||||
- env
|
||||
- sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION"
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: orchard
|
||||
password:
|
||||
secure: M8UMupCLSsB1hV00Zn6ra8Vg81SCFBpbcRsa0nUw9kgXn9hOCESWYVHTqQ1ksWZOa8z6WMaqYtoosPKXGJQNf0wF/kEVDsMUeaZWOF/PqDkx1EwQ1diVfwlbN4/k0iX+Se7SrZfiWnJiAqiIPqToQipvLlJohqf8WwfPcVvILVE=
|
||||
on:
|
||||
tags: true
|
||||
repo: orchardup/fig
|
||||
|
||||
66
CHANGES.md
Normal file
66
CHANGES.md
Normal file
@@ -0,0 +1,66 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
0.2.0 (2014-01-31)
|
||||
------------------
|
||||
|
||||
- Link services to themselves so run commands can access the running service. (#67)
|
||||
- Much better documentation.
|
||||
- Make service dependency resolution more reliable. (#48)
|
||||
- Load Fig configurations with a `.yaml` extension. (#58)
|
||||
|
||||
Big thanks to @cameronmaske, @mrchrisadams and @damianmoore for their help with this release.
|
||||
|
||||
0.1.4 (2014-01-27)
|
||||
------------------
|
||||
|
||||
- Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54)
|
||||
|
||||
0.1.3 (2014-01-23)
|
||||
------------------
|
||||
|
||||
- Fix ports sometimes being configured incorrectly. (#46)
|
||||
- Fix log output sometimes not displaying. (#47)
|
||||
|
||||
0.1.2 (2014-01-22)
|
||||
------------------
|
||||
|
||||
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
|
||||
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
|
||||
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
|
||||
|
||||
0.1.1 (2014-01-17)
|
||||
------------------
|
||||
|
||||
- Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell!
|
||||
|
||||
0.1.0 (2014-01-16)
|
||||
------------------
|
||||
|
||||
- Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2)
|
||||
- Add `fig scale` command (#9)
|
||||
- Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19)
|
||||
- Truncate long commands in `fig ps` (#18)
|
||||
- Fill out CLI help banners for commands (#15, #16)
|
||||
- Show a friendlier error when `fig.yml` is missing (#4)
|
||||
- Fix bug with `fig build` logging (#3)
|
||||
- Fix bug where builds would time out if a step took a long time without generating output (#6)
|
||||
- Fix bug where streaming container output over the Unix socket raised an error (#7)
|
||||
|
||||
Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt.
|
||||
|
||||
0.0.2 (2014-01-02)
|
||||
------------------
|
||||
|
||||
- Improve documentation
|
||||
- Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`.
|
||||
- Improve `fig up` behaviour
|
||||
- Add confirmation prompt to `fig rm`
|
||||
- Add `fig build` command
|
||||
|
||||
0.0.1 (2013-12-20)
|
||||
------------------
|
||||
|
||||
Initial release.
|
||||
|
||||
|
||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM stackbrew/ubuntu:12.04
|
||||
RUN apt-get update -qq
|
||||
RUN apt-get install -y python python-pip
|
||||
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/
|
||||
@@ -1,3 +1,10 @@
|
||||
include Dockerfile
|
||||
include LICENSE
|
||||
include *.md
|
||||
include requirements.txt
|
||||
include requirements-dev.txt
|
||||
tox.ini
|
||||
include *.md
|
||||
recursive-include tests *
|
||||
global-exclude *.pyc
|
||||
global-exclude *.pyo
|
||||
global-exclude *.un~
|
||||
|
||||
174
README.md
174
README.md
@@ -1,163 +1,55 @@
|
||||
Fig
|
||||
====
|
||||
===
|
||||
|
||||
Punctual, lightweight development environments using Docker.
|
||||
[](https://travis-ci.org/orchardup/fig)
|
||||
[](http://badge.fury.io/py/fig)
|
||||
|
||||
Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this:
|
||||
Fast, isolated development environments using Docker.
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
ports:
|
||||
- 8000:8000
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
Define your app's environment with Docker so it can be reproduced anywhere:
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
```bash
|
||||
$ sudo pip install fig
|
||||
```
|
||||
|
||||
Defining your app
|
||||
-----------------
|
||||
|
||||
Put a `fig.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use.
|
||||
|
||||
The simplest way to get started is to just give it an image name:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
|
||||
You've now given Fig the minimal amount of configuration it needs to run:
|
||||
|
||||
```bash
|
||||
$ fig up
|
||||
Pulling image orchardup/postgresql...
|
||||
Starting myapp_db_1...
|
||||
myapp_db_1 is running at 127.0.0.1:45678
|
||||
<...output from postgresql server...>
|
||||
```
|
||||
|
||||
For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`.
|
||||
|
||||
By default, `fig up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag:
|
||||
|
||||
```bash
|
||||
$ fig up -d
|
||||
Starting myapp_db_1... done
|
||||
myapp_db_1 is running at 127.0.0.1:45678
|
||||
|
||||
$ fig ps
|
||||
Name State Ports
|
||||
------------------------------------
|
||||
myapp_db_1 Up 5432->45678/tcp
|
||||
```
|
||||
|
||||
### Building services
|
||||
|
||||
Fig can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command).
|
||||
|
||||
This example will build an image with `app.py` inside it:
|
||||
|
||||
#### app.py
|
||||
|
||||
```python
|
||||
print "Hello world!"
|
||||
```
|
||||
|
||||
#### fig.yml
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
```
|
||||
|
||||
#### Dockerfile
|
||||
|
||||
FROM ubuntu:12.04
|
||||
RUN apt-get install python
|
||||
ADD . /opt
|
||||
WORKDIR /opt
|
||||
FROM orchardup/python:2.7
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
CMD python app.py
|
||||
|
||||
|
||||
|
||||
### Getting your code in
|
||||
|
||||
If you want to work on an application being run by Fig, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately:
|
||||
Define the services that make up your app so they can be run together in an isolated environment:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/opt
|
||||
```
|
||||
|
||||
|
||||
### Communicating between containers
|
||||
|
||||
Your web app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
```
|
||||
|
||||
This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container:
|
||||
|
||||
```bash
|
||||
$ fig up -d db
|
||||
$ fig run web env
|
||||
```
|
||||
|
||||
|
||||
### Container configuration options
|
||||
|
||||
You can pass extra configuration options to a container, much like with `docker run`:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
|
||||
-- override the default command
|
||||
command: bundle exec thin -p 3000
|
||||
|
||||
-- expose ports, optionally specifying both host and container ports (a random host port will be chosen otherwise)
|
||||
ports:
|
||||
- 3000
|
||||
- 8000:8000
|
||||
|
||||
-- map volumes
|
||||
volumes:
|
||||
- cache/:/tmp/cache
|
||||
|
||||
-- add environment variables
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
|
||||
(No more installing Postgres on your laptop!)
|
||||
|
||||
Running a one-off command
|
||||
-------------------------
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
|
||||
If you want to run a management command, use `fig run` to start a one-off container:
|
||||

|
||||
|
||||
```bash
|
||||
$ fig run db createdb myapp_development
|
||||
$ fig run web rake db:migrate
|
||||
$ fig run web bash
|
||||
```
|
||||
There are commands to:
|
||||
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
|
||||
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
|
||||
------------------------------
|
||||
|
||||
Full documentation is available on [Fig's website](http://orchardup.github.io/fig/).
|
||||
|
||||
Running the test suite
|
||||
----------------------
|
||||
|
||||
$ script/test
|
||||
|
||||
|
||||
|
||||
1
docs/.gitignore-gh-pages
Normal file
1
docs/.gitignore-gh-pages
Normal file
@@ -0,0 +1 @@
|
||||
/_site
|
||||
10
docs/Dockerfile
Normal file
10
docs/Dockerfile
Normal 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
3
docs/Gemfile
Normal file
@@ -0,0 +1,3 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'github-pages'
|
||||
62
docs/Gemfile.lock
Normal file
62
docs/Gemfile.lock
Normal 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
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
markdown: redcarpet
|
||||
69
docs/_layouts/default.html
Normal file
69
docs/_layouts/default.html
Normal 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&repo=fig&type=watch&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
81
docs/cli.md
Normal 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
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
180
docs/css/fig.css
Normal 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
93
docs/django.md
Normal 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
31
docs/env.md
Normal 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
8
docs/fig.yml
Normal 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
BIN
docs/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
157
docs/index.md
Normal file
157
docs/index.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
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/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 don’t 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
23
docs/install.md
Normal 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/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 don’t 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
99
docs/rails.md
Normal 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).
|
||||
|
||||

|
||||
92
docs/wordpress.md
Normal file
92
docs/wordpress.md
Normal 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
43
docs/yml.md
Normal 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
|
||||
```
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service
|
||||
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
NAMES = [
|
||||
'grey',
|
||||
'red',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from docker import Client
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from ..packages.docker import Client
|
||||
from requests.exceptions import ConnectionError
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -7,21 +11,42 @@ import yaml
|
||||
from ..project import Project
|
||||
from .docopt_command import DocoptCommand
|
||||
from .formatter import Formatter
|
||||
from .utils import cached_property, mkdir
|
||||
from .utils import cached_property, docker_url
|
||||
from .errors import UserError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Command(DocoptCommand):
|
||||
base_dir = '.'
|
||||
|
||||
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 it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
||||
""" % self.client.base_url)
|
||||
|
||||
@cached_property
|
||||
def client(self):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
return Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
return Client()
|
||||
return Client(docker_url())
|
||||
|
||||
@cached_property
|
||||
def project(self):
|
||||
config = yaml.load(open('fig.yml'))
|
||||
try:
|
||||
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)
|
||||
|
||||
exit(1)
|
||||
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
|
||||
@cached_property
|
||||
@@ -36,3 +61,12 @@ class Command(DocoptCommand):
|
||||
def formatter(self):
|
||||
return Formatter()
|
||||
|
||||
def check_yaml_filename(self):
|
||||
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
|
||||
|
||||
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')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from inspect import getdoc
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import absolute_import
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = dedent(msg).strip()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.msg
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import texttable
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import texttable
|
||||
|
||||
|
||||
class Formatter(object):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import sys
|
||||
|
||||
from itertools import cycle
|
||||
|
||||
from .multiplexer import Multiplexer
|
||||
from . import colors
|
||||
from .utils import split_buffer
|
||||
|
||||
|
||||
class LogPrinter(object):
|
||||
@@ -29,38 +32,16 @@ class LogPrinter(object):
|
||||
|
||||
def _make_log_generator(self, container, color_fn):
|
||||
prefix = color_fn(container.name + " | ")
|
||||
websocket = self._attach(container)
|
||||
return (prefix + line for line in split_buffer(read_websocket(websocket), '\n'))
|
||||
# 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 = {
|
||||
'stdin': False,
|
||||
'stdout': True,
|
||||
'stderr': True,
|
||||
'logs': False,
|
||||
'stream': True,
|
||||
}
|
||||
params.update(self.attach_params)
|
||||
params = dict((name, 1 if value else 0) for (name, value) in params.items())
|
||||
return container.attach_socket(params=params, ws=True)
|
||||
|
||||
def read_websocket(websocket):
|
||||
while True:
|
||||
data = websocket.recv()
|
||||
if data:
|
||||
yield data
|
||||
else:
|
||||
break
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
buffered = ''
|
||||
|
||||
for data in reader:
|
||||
lines = (buffered + data).split(separator)
|
||||
for line in lines[:-1]:
|
||||
yield line + separator
|
||||
if len(lines) > 1:
|
||||
buffered = lines[-1]
|
||||
|
||||
if len(buffered) > 0:
|
||||
yield buffered
|
||||
params = dict((name, 1 if value else 0) for (name, value) in list(params.items()))
|
||||
return container.attach(**params)
|
||||
|
||||
260
fig/cli/main.py
260
fig/cli/main.py
@@ -1,16 +1,21 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import sys
|
||||
import re
|
||||
import signal
|
||||
|
||||
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
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import yesno
|
||||
|
||||
from docker.client import APIError
|
||||
from ..packages.docker.client import APIError
|
||||
from .errors import UserError
|
||||
from .docopt_command import NoSuchCommand
|
||||
from .socketclient import SocketClient
|
||||
@@ -19,7 +24,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler = logging.StreamHandler(stream=sys.stderr)
|
||||
console_handler.setFormatter(logging.Formatter())
|
||||
console_handler.setLevel(logging.INFO)
|
||||
root_logger = logging.getLogger()
|
||||
@@ -35,18 +40,15 @@ def main():
|
||||
except KeyboardInterrupt:
|
||||
log.error("\nAborting.")
|
||||
exit(1)
|
||||
except UserError, e:
|
||||
except (UserError, NoSuchService, DependencyError) as e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchService, e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchCommand, e:
|
||||
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)
|
||||
except APIError, e:
|
||||
except APIError as e:
|
||||
log.error(e.explanation)
|
||||
exit(1)
|
||||
|
||||
@@ -70,14 +72,17 @@ class TopLevelCommand(Command):
|
||||
--version Print version and exit
|
||||
|
||||
Commands:
|
||||
up Create and start containers
|
||||
build Build or rebuild services
|
||||
help Get help on a command
|
||||
kill Kill containers
|
||||
logs View output from containers
|
||||
ps List containers
|
||||
rm Remove stopped containers
|
||||
run Run a one-off command
|
||||
scale Set number of containers for a service
|
||||
start Start services
|
||||
stop Stop services
|
||||
kill Kill containers
|
||||
rm Remove stopped containers
|
||||
up Create and start containers
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
@@ -85,6 +90,47 @@ class TopLevelCommand(Command):
|
||||
options['version'] = "fig %s" % __version__
|
||||
return options
|
||||
|
||||
def build(self, options):
|
||||
"""
|
||||
Build or rebuild services.
|
||||
|
||||
Services are built once and then tagged as `project_service`,
|
||||
e.g. `figtest_db`. If you change a service's `Dockerfile` or the
|
||||
contents of its build directory, you can run `fig build` to rebuild it.
|
||||
|
||||
Usage: build [SERVICE...]
|
||||
"""
|
||||
self.project.build(service_names=options['SERVICE'])
|
||||
|
||||
def help(self, options):
|
||||
"""
|
||||
Get help on a command.
|
||||
|
||||
Usage: help COMMAND
|
||||
"""
|
||||
command = options['COMMAND']
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
raise SystemExit(getdoc(getattr(self, command)))
|
||||
|
||||
def kill(self, options):
|
||||
"""
|
||||
Force stop service containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
|
||||
def logs(self, options):
|
||||
"""
|
||||
View output from containers.
|
||||
|
||||
Usage: logs [SERVICE...]
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
print("Attaching to", list_containers(containers))
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
|
||||
def ps(self, options):
|
||||
"""
|
||||
List containers.
|
||||
@@ -98,7 +144,7 @@ class TopLevelCommand(Command):
|
||||
|
||||
if options['-q']:
|
||||
for container in containers:
|
||||
print container.id
|
||||
print(container.id)
|
||||
else:
|
||||
headers = [
|
||||
'Name',
|
||||
@@ -108,72 +154,97 @@ class TopLevelCommand(Command):
|
||||
]
|
||||
rows = []
|
||||
for container in containers:
|
||||
command = container.human_readable_command
|
||||
if len(command) > 30:
|
||||
command = '%s ...' % command[:26]
|
||||
rows.append([
|
||||
container.name,
|
||||
container.human_readable_command,
|
||||
command,
|
||||
container.human_readable_state,
|
||||
container.human_readable_ports,
|
||||
])
|
||||
print Formatter().table(headers, rows)
|
||||
print(Formatter().table(headers, rows))
|
||||
|
||||
def rm(self, options):
|
||||
"""
|
||||
Remove stopped service containers.
|
||||
|
||||
Usage: rm [SERVICE...]
|
||||
"""
|
||||
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
stopped_containers = [c for c in all_containers if not c.is_running]
|
||||
|
||||
if len(stopped_containers) > 0:
|
||||
print("Going to remove", list_containers(stopped_containers))
|
||||
if yesno("Are you sure? [yN] ", default=False):
|
||||
self.project.remove_stopped(service_names=options['SERVICE'])
|
||||
else:
|
||||
print("No stopped containers")
|
||||
|
||||
def run(self, options):
|
||||
"""
|
||||
Run a one-off command.
|
||||
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.
|
||||
|
||||
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)
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None)
|
||||
print container.name
|
||||
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()
|
||||
|
||||
def up(self, options):
|
||||
def scale(self, options):
|
||||
"""
|
||||
Create and start containers.
|
||||
Set number of containers to run for a service.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
Numbers are specified in the form `service=num` as arguments.
|
||||
For example:
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new container names
|
||||
$ fig scale web=2 worker=3
|
||||
|
||||
Usage: scale [SERVICE=NUM...]
|
||||
"""
|
||||
detached = options['-d']
|
||||
|
||||
unstarted = self.project.create_containers(service_names=options['SERVICE'])
|
||||
|
||||
if not detached:
|
||||
to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted]
|
||||
print "Attaching to", list_containers(to_attach)
|
||||
log_printer = LogPrinter(to_attach, attach_params={'logs': True})
|
||||
|
||||
for (s, c) in unstarted:
|
||||
s.start_container(c)
|
||||
|
||||
if detached:
|
||||
for (s, c) in unstarted:
|
||||
print c.name
|
||||
else:
|
||||
for s in options['SERVICE=NUM']:
|
||||
if '=' not in s:
|
||||
raise UserError('Arguments to scale should be in the form service=num')
|
||||
service_name, num = s.split('=', 1)
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
self.project.kill_and_remove(unstarted)
|
||||
num = int(num)
|
||||
except ValueError:
|
||||
raise UserError('Number of containers for service "%s" is not a number' % service)
|
||||
try:
|
||||
self.project.get_service(service_name).scale(num)
|
||||
except CannotBeScaledError:
|
||||
raise UserError('Service "%s" cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash.\n\nRemove the ":" from the port definition in fig.yml so Docker can choose a random port for each container.' % service_name)
|
||||
|
||||
|
||||
def start(self, options):
|
||||
"""
|
||||
@@ -185,67 +256,68 @@ class TopLevelCommand(Command):
|
||||
|
||||
def stop(self, options):
|
||||
"""
|
||||
Stop running containers.
|
||||
Stop running containers without removing them.
|
||||
|
||||
They can be started again with `fig start`.
|
||||
|
||||
Usage: stop [SERVICE...]
|
||||
"""
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def kill(self, options):
|
||||
def up(self, options):
|
||||
"""
|
||||
Kill containers.
|
||||
Build, (re)create, start and attach to containers for a service.
|
||||
|
||||
Usage: kill [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.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new
|
||||
container names
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
detached = options['-d']
|
||||
|
||||
def rm(self, options):
|
||||
"""
|
||||
Remove stopped containers
|
||||
(old, new) = self.project.recreate_containers(service_names=options['SERVICE'])
|
||||
|
||||
Usage: rm [SERVICE...]
|
||||
"""
|
||||
self.project.remove_stopped(service_names=options['SERVICE'])
|
||||
if not detached:
|
||||
to_attach = [c for (s, c) in new]
|
||||
print("Attaching to", list_containers(to_attach))
|
||||
log_printer = LogPrinter(to_attach)
|
||||
|
||||
def logs(self, options):
|
||||
"""
|
||||
View output from containers.
|
||||
for (service, container) in new:
|
||||
service.start_container(container)
|
||||
|
||||
Usage: logs [SERVICE...]
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=False)
|
||||
print "Attaching to", list_containers(containers)
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
for (service, container) in old:
|
||||
container.remove()
|
||||
|
||||
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,
|
||||
)
|
||||
if not detached:
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
def handler(signal, frame):
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
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,
|
||||
)
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def _attach_to_container(self, container_id, raw=False):
|
||||
socket_in = self.client.attach_socket(container_id, params={'stdin': 1, 'stream': 1})
|
||||
socket_out = self.client.attach_socket(container_id, params={'stdout': 1, 'logs': 1, 'stream': 1})
|
||||
socket_err = self.client.attach_socket(container_id, params={'stderr': 1, 'logs': 1, 'stream': 1})
|
||||
|
||||
return SocketClient(
|
||||
socket_in=stdio,
|
||||
socket_out=stdio,
|
||||
socket_err=stderr,
|
||||
socket_in=socket_in,
|
||||
socket_out=socket_out,
|
||||
socket_err=socket_err,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from threading import Thread
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,6 +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
|
||||
@@ -56,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()
|
||||
@@ -75,37 +75,34 @@ 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)
|
||||
stream.flush()
|
||||
else:
|
||||
break
|
||||
except Exception, e:
|
||||
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, 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:
|
||||
@@ -123,7 +120,7 @@ if __name__ == '__main__':
|
||||
url = sys.argv[1]
|
||||
socket = websocket.create_connection(url)
|
||||
|
||||
print "connected\r"
|
||||
print("connected\r")
|
||||
|
||||
with SocketClient(socket, interactive=True) as client:
|
||||
client.run()
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
import datetime
|
||||
import os
|
||||
import socket
|
||||
from .errors import UserError
|
||||
|
||||
|
||||
def cached_property(f):
|
||||
@@ -67,10 +72,39 @@ def prettydate(d):
|
||||
return '{0} hours ago'.format(s/3600)
|
||||
|
||||
|
||||
def mkdir(path, permissions=0700):
|
||||
def mkdir(path, permissions=0o700):
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
|
||||
os.chmod(path, permissions)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def 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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from __future__ import unicode_literals
|
||||
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):
|
||||
@@ -39,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]
|
||||
@@ -47,13 +50,24 @@ 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:
|
||||
return int(self.name.split('_')[-1])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def human_readable_ports(self):
|
||||
self.inspect_if_not_inspected()
|
||||
if not self.dictionary['NetworkSettings']['Ports']:
|
||||
return ''
|
||||
ports = []
|
||||
for private, public in self.dictionary['NetworkSettings']['Ports'].items():
|
||||
for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()):
|
||||
if public:
|
||||
ports.append('%s->%s' % (public[0]['HostPort'], private))
|
||||
return ', '.join(ports)
|
||||
@@ -89,19 +103,15 @@ class Container(object):
|
||||
return self.dictionary['State']['Running']
|
||||
|
||||
def start(self, **options):
|
||||
log.info("Starting %s..." % self.name)
|
||||
return self.client.start(self.id, **options)
|
||||
|
||||
def stop(self, **options):
|
||||
log.info("Stopping %s..." % self.name)
|
||||
return self.client.stop(self.id, **options)
|
||||
|
||||
def kill(self):
|
||||
log.info("Killing %s..." % self.name)
|
||||
return self.client.kill(self.id)
|
||||
|
||||
def remove(self):
|
||||
log.info("Removing %s..." % self.name)
|
||||
return self.client.remove_container(self.id)
|
||||
|
||||
def inspect_if_not_inspected(self):
|
||||
@@ -127,6 +137,9 @@ class Container(object):
|
||||
links.append(bits[2])
|
||||
return links
|
||||
|
||||
def attach(self, *args, **kwargs):
|
||||
return self.client.attach(self.id, *args, **kwargs)
|
||||
|
||||
def attach_socket(self, **kwargs):
|
||||
return self.client.attach_socket(self.id, **kwargs)
|
||||
|
||||
|
||||
0
fig/packages/__init__.py
Normal file
0
fig/packages/__init__.py
Normal file
15
fig/packages/docker/__init__.py
Normal file
15
fig/packages/docker/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .client import Client, APIError # flake8: noqa
|
||||
7
fig/packages/docker/auth/__init__.py
Normal file
7
fig/packages/docker/auth/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .auth import (
|
||||
INDEX_URL,
|
||||
encode_header,
|
||||
load_config,
|
||||
resolve_authconfig,
|
||||
resolve_repository_name
|
||||
) # flake8: noqa
|
||||
153
fig/packages/docker/auth/auth.py
Normal file
153
fig/packages/docker/auth/auth.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import base64
|
||||
import fileinput
|
||||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from ..utils import utils
|
||||
|
||||
INDEX_URL = 'https://index.docker.io/v1/'
|
||||
DOCKER_CONFIG_FILENAME = '.dockercfg'
|
||||
|
||||
|
||||
def swap_protocol(url):
|
||||
if url.startswith('http://'):
|
||||
return url.replace('http://', 'https://', 1)
|
||||
if url.startswith('https://'):
|
||||
return url.replace('https://', 'http://', 1)
|
||||
return url
|
||||
|
||||
|
||||
def expand_registry_url(hostname):
|
||||
if hostname.startswith('http:') or hostname.startswith('https:'):
|
||||
if '/' not in hostname[9:]:
|
||||
hostname = hostname + '/v1/'
|
||||
return hostname
|
||||
if utils.ping('https://' + hostname + '/v1/_ping'):
|
||||
return 'https://' + hostname + '/v1/'
|
||||
return 'http://' + hostname + '/v1/'
|
||||
|
||||
|
||||
def resolve_repository_name(repo_name):
|
||||
if '://' in repo_name:
|
||||
raise ValueError('Repository name cannot contain a '
|
||||
'scheme ({0})'.format(repo_name))
|
||||
parts = repo_name.split('/', 1)
|
||||
if not '.' in parts[0] and not ':' in parts[0] and parts[0] != 'localhost':
|
||||
# This is a docker index repo (ex: foo/bar or ubuntu)
|
||||
return INDEX_URL, repo_name
|
||||
if len(parts) < 2:
|
||||
raise ValueError('Invalid repository name ({0})'.format(repo_name))
|
||||
|
||||
if 'index.docker.io' in parts[0]:
|
||||
raise ValueError('Invalid repository name,'
|
||||
'try "{0}" instead'.format(parts[1]))
|
||||
|
||||
return expand_registry_url(parts[0]), parts[1]
|
||||
|
||||
|
||||
def resolve_authconfig(authconfig, registry=None):
|
||||
"""Return the authentication data from the given auth configuration for a
|
||||
specific registry. We'll do our best to infer the correct URL for the
|
||||
registry, trying both http and https schemes. Returns an empty dictionnary
|
||||
if no data exists."""
|
||||
# Default to the public index server
|
||||
registry = registry or INDEX_URL
|
||||
|
||||
# Ff its not the index server there are three cases:
|
||||
#
|
||||
# 1. this is a full config url -> it should be used as is
|
||||
# 2. it could be a full url, but with the wrong protocol
|
||||
# 3. it can be the hostname optionally with a port
|
||||
#
|
||||
# as there is only one auth entry which is fully qualified we need to start
|
||||
# parsing and matching
|
||||
if '/' not in registry:
|
||||
registry = registry + '/v1/'
|
||||
if not registry.startswith('http:') and not registry.startswith('https:'):
|
||||
registry = 'https://' + registry
|
||||
|
||||
if registry in authconfig:
|
||||
return authconfig[registry]
|
||||
return authconfig.get(swap_protocol(registry), None)
|
||||
|
||||
|
||||
def decode_auth(auth):
|
||||
if isinstance(auth, six.string_types):
|
||||
auth = auth.encode('ascii')
|
||||
s = base64.b64decode(auth)
|
||||
login, pwd = s.split(b':')
|
||||
return login.decode('ascii'), pwd.decode('ascii')
|
||||
|
||||
|
||||
def encode_header(auth):
|
||||
auth_json = json.dumps(auth).encode('ascii')
|
||||
return base64.b64encode(auth_json)
|
||||
|
||||
|
||||
def load_config(root=None):
|
||||
"""Loads authentication data from a Docker configuration file in the given
|
||||
root directory."""
|
||||
conf = {}
|
||||
data = None
|
||||
|
||||
config_file = os.path.join(root or os.environ.get('HOME', '.'),
|
||||
DOCKER_CONFIG_FILENAME)
|
||||
|
||||
# First try as JSON
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
conf = {}
|
||||
for registry, entry in six.iteritems(json.load(f)):
|
||||
username, password = decode_auth(entry['auth'])
|
||||
conf[registry] = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': entry['email'],
|
||||
'serveraddress': registry,
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
pass
|
||||
|
||||
# If that fails, we assume the configuration file contains a single
|
||||
# authentication token for the public registry in the following format:
|
||||
#
|
||||
# auth = AUTH_TOKEN
|
||||
# email = email@domain.com
|
||||
try:
|
||||
data = []
|
||||
for line in fileinput.input(config_file):
|
||||
data.append(line.strip().split(' = ')[1])
|
||||
if len(data) < 2:
|
||||
# Not enough data
|
||||
raise Exception('Invalid or empty configuration file!')
|
||||
|
||||
username, password = decode_auth(data[0])
|
||||
conf[INDEX_URL] = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': data[1],
|
||||
'serveraddress': INDEX_URL,
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
pass
|
||||
|
||||
# If all fails, return an empty config
|
||||
return {}
|
||||
764
fig/packages/docker/client.py
Normal file
764
fig/packages/docker/client.py
Normal file
@@ -0,0 +1,764 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import struct
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import six
|
||||
|
||||
from .auth import auth
|
||||
from .unixconn import unixconn
|
||||
from .utils import utils
|
||||
|
||||
if not six.PY3:
|
||||
import websocket
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 60
|
||||
STREAM_HEADER_SIZE_BYTES = 8
|
||||
|
||||
|
||||
class APIError(requests.exceptions.HTTPError):
|
||||
def __init__(self, message, response, explanation=None):
|
||||
super(APIError, self).__init__(message, response=response)
|
||||
|
||||
self.explanation = explanation
|
||||
|
||||
if self.explanation is None and response.content:
|
||||
self.explanation = response.content.strip()
|
||||
|
||||
def __str__(self):
|
||||
message = super(APIError, self).__str__()
|
||||
|
||||
if self.is_client_error():
|
||||
message = '%s Client Error: %s' % (
|
||||
self.response.status_code, self.response.reason)
|
||||
|
||||
elif self.is_server_error():
|
||||
message = '%s Server Error: %s' % (
|
||||
self.response.status_code, self.response.reason)
|
||||
|
||||
if self.explanation:
|
||||
message = '%s ("%s")' % (message, self.explanation)
|
||||
|
||||
return message
|
||||
|
||||
def is_client_error(self):
|
||||
return 400 <= self.response.status_code < 500
|
||||
|
||||
def is_server_error(self):
|
||||
return 500 <= self.response.status_code < 600
|
||||
|
||||
|
||||
class Client(requests.Session):
|
||||
def __init__(self, base_url=None, version="1.6",
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS):
|
||||
super(Client, self).__init__()
|
||||
if base_url is None:
|
||||
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('/'):
|
||||
base_url = base_url[:-1]
|
||||
self.base_url = base_url
|
||||
self._version = version
|
||||
self._timeout = timeout
|
||||
self._auth_configs = auth.load_config()
|
||||
|
||||
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
|
||||
parameter, if not already present."""
|
||||
kwargs.setdefault('timeout', self._timeout)
|
||||
return kwargs
|
||||
|
||||
def _post(self, url, **kwargs):
|
||||
return self.post(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _get(self, url, **kwargs):
|
||||
return self.get(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _delete(self, url, **kwargs):
|
||||
return self.delete(url, **self._set_request_timeout(kwargs))
|
||||
|
||||
def _url(self, path):
|
||||
return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
|
||||
|
||||
def _raise_for_status(self, response, explanation=None):
|
||||
"""Raises stored :class:`APIError`, if one occurred."""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise APIError(e, response, explanation=explanation)
|
||||
|
||||
def _result(self, response, json=False, binary=False):
|
||||
assert not (json and binary)
|
||||
self._raise_for_status(response)
|
||||
|
||||
if json:
|
||||
return response.json()
|
||||
if binary:
|
||||
return response.content
|
||||
return response.text
|
||||
|
||||
def _container_config(self, image, command, hostname=None, user=None,
|
||||
detach=False, stdin_open=False, tty=False,
|
||||
mem_limit=0, ports=None, environment=None, dns=None,
|
||||
volumes=None, volumes_from=None,
|
||||
network_disabled=False, entrypoint=None,
|
||||
cpu_shares=None, working_dir=None):
|
||||
if isinstance(command, six.string_types):
|
||||
command = shlex.split(str(command))
|
||||
if isinstance(environment, dict):
|
||||
environment = [
|
||||
'{0}={1}'.format(k, v) for k, v in environment.items()
|
||||
]
|
||||
|
||||
if ports and isinstance(ports, list):
|
||||
exposed_ports = {}
|
||||
for port_definition in ports:
|
||||
port = port_definition
|
||||
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)] = {}
|
||||
ports = exposed_ports
|
||||
|
||||
if volumes and isinstance(volumes, list):
|
||||
volumes_dict = {}
|
||||
for vol in volumes:
|
||||
volumes_dict[vol] = {}
|
||||
volumes = volumes_dict
|
||||
|
||||
attach_stdin = False
|
||||
attach_stdout = False
|
||||
attach_stderr = False
|
||||
stdin_once = False
|
||||
|
||||
if not detach:
|
||||
attach_stdout = True
|
||||
attach_stderr = True
|
||||
|
||||
if stdin_open:
|
||||
attach_stdin = True
|
||||
stdin_once = True
|
||||
|
||||
return {
|
||||
'Hostname': hostname,
|
||||
'ExposedPorts': ports,
|
||||
'User': user,
|
||||
'Tty': tty,
|
||||
'OpenStdin': stdin_open,
|
||||
'StdinOnce': stdin_once,
|
||||
'Memory': mem_limit,
|
||||
'AttachStdin': attach_stdin,
|
||||
'AttachStdout': attach_stdout,
|
||||
'AttachStderr': attach_stderr,
|
||||
'Env': environment,
|
||||
'Cmd': command,
|
||||
'Dns': dns,
|
||||
'Image': image,
|
||||
'Volumes': volumes,
|
||||
'VolumesFrom': volumes_from,
|
||||
'NetworkDisabled': network_disabled,
|
||||
'Entrypoint': entrypoint,
|
||||
'CpuShares': cpu_shares,
|
||||
'WorkingDir': working_dir
|
||||
}
|
||||
|
||||
def _post_json(self, url, data, **kwargs):
|
||||
# Go <1.1 can't unserialize null to a string
|
||||
# so we do this disgusting thing here.
|
||||
data2 = {}
|
||||
if data is not None:
|
||||
for k, v in six.iteritems(data):
|
||||
if v is not None:
|
||||
data2[k] = v
|
||||
|
||||
if 'headers' not in kwargs:
|
||||
kwargs['headers'] = {}
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
return self._post(url, data=json.dumps(data2), **kwargs)
|
||||
|
||||
def _attach_params(self, override=None):
|
||||
return override or {
|
||||
'stdout': 1,
|
||||
'stderr': 1,
|
||||
'stream': 1
|
||||
}
|
||||
|
||||
def _attach_websocket(self, container, params=None):
|
||||
if six.PY3:
|
||||
raise NotImplementedError("This method is not currently supported "
|
||||
"under python 3")
|
||||
url = self._url("/containers/{0}/attach/ws".format(container))
|
||||
req = requests.Request("POST", url, params=self._attach_params(params))
|
||||
full_url = req.prepare().url
|
||||
full_url = full_url.replace("http://", "ws://", 1)
|
||||
full_url = full_url.replace("https://", "wss://", 1)
|
||||
return self._create_websocket_connection(full_url)
|
||||
|
||||
def _create_websocket_connection(self, url):
|
||||
return websocket.create_connection(url)
|
||||
|
||||
def _stream_result(self, response):
|
||||
"""Generator for straight-out, non chunked-encoded HTTP responses."""
|
||||
self._raise_for_status(response)
|
||||
for line in response.iter_lines(chunk_size=1, decode_unicode=True):
|
||||
# filter out keep-alive new lines
|
||||
if line:
|
||||
yield line + '\n'
|
||||
|
||||
def _stream_result_socket(self, response):
|
||||
self._raise_for_status(response)
|
||||
return response.raw._fp.fp._sock
|
||||
|
||||
def _stream_helper(self, response):
|
||||
"""Generator for data coming from a chunked-encoded HTTP response."""
|
||||
socket_fp = self._stream_result_socket(response)
|
||||
socket_fp.setblocking(1)
|
||||
socket = socket_fp.makefile()
|
||||
while True:
|
||||
size = int(socket.readline(), 16)
|
||||
if size <= 0:
|
||||
break
|
||||
data = socket.readline()
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
|
||||
def _multiplexed_buffer_helper(self, response):
|
||||
"""A generator of multiplexed data blocks read from a buffered
|
||||
response."""
|
||||
buf = self._result(response, binary=True)
|
||||
walker = 0
|
||||
while True:
|
||||
if len(buf[walker:]) < 8:
|
||||
break
|
||||
_, length = struct.unpack_from('>BxxxL', buf[walker:])
|
||||
start = walker + STREAM_HEADER_SIZE_BYTES
|
||||
end = start + length
|
||||
walker = end
|
||||
yield str(buf[start:end])
|
||||
|
||||
def _multiplexed_socket_stream_helper(self, response):
|
||||
"""A generator of multiplexed data blocks coming from a response
|
||||
socket."""
|
||||
socket = self._stream_result_socket(response)
|
||||
|
||||
def recvall(socket, size):
|
||||
data = ''
|
||||
while size > 0:
|
||||
block = socket.recv(size)
|
||||
if not block:
|
||||
return None
|
||||
|
||||
data += block
|
||||
size -= len(block)
|
||||
return data
|
||||
|
||||
while True:
|
||||
socket.settimeout(None)
|
||||
header = recvall(socket, STREAM_HEADER_SIZE_BYTES)
|
||||
if not header:
|
||||
break
|
||||
_, length = struct.unpack('>BxxxL', header)
|
||||
if not length:
|
||||
break
|
||||
data = recvall(socket, length)
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
|
||||
def attach(self, container, stdout=True, stderr=True,
|
||||
stream=False, logs=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {
|
||||
'logs': logs and 1 or 0,
|
||||
'stdout': stdout and 1 or 0,
|
||||
'stderr': stderr and 1 or 0,
|
||||
'stream': stream and 1 or 0,
|
||||
}
|
||||
u = self._url("/containers/{0}/attach".format(container))
|
||||
response = self._post(u, params=params, stream=stream)
|
||||
|
||||
# Stream multi-plexing was introduced in API v1.6.
|
||||
if utils.compare_version('1.6', self._version) < 0:
|
||||
return stream and self._stream_result(response) or \
|
||||
self._result(response, binary=True)
|
||||
|
||||
return stream and self._multiplexed_socket_stream_helper(response) or \
|
||||
''.join([x for x in self._multiplexed_buffer_helper(response)])
|
||||
|
||||
def attach_socket(self, container, params=None, ws=False):
|
||||
if params is None:
|
||||
params = {
|
||||
'stdout': 1,
|
||||
'stderr': 1,
|
||||
'stream': 1
|
||||
}
|
||||
if ws:
|
||||
return self._attach_websocket(container, params)
|
||||
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
u = self._url("/containers/{0}/attach".format(container))
|
||||
return self._stream_result_socket(self.post(
|
||||
u, None, params=self._attach_params(params), stream=True))
|
||||
|
||||
def build(self, path=None, tag=None, quiet=False, fileobj=None,
|
||||
nocache=False, rm=False, stream=False, timeout=None):
|
||||
remote = context = headers = None
|
||||
if path is None and fileobj is None:
|
||||
raise Exception("Either path or fileobj needs to be provided.")
|
||||
|
||||
if fileobj is not None:
|
||||
context = utils.mkbuildcontext(fileobj)
|
||||
elif path.startswith(('http://', 'https://', 'git://', 'github.com/')):
|
||||
remote = path
|
||||
else:
|
||||
context = utils.tar(path)
|
||||
|
||||
u = self._url('/build')
|
||||
params = {
|
||||
't': tag,
|
||||
'remote': remote,
|
||||
'q': quiet,
|
||||
'nocache': nocache,
|
||||
'rm': rm
|
||||
}
|
||||
if context is not None:
|
||||
headers = {'Content-Type': 'application/tar'}
|
||||
|
||||
response = self._post(
|
||||
u,
|
||||
data=context,
|
||||
params=params,
|
||||
headers=headers,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if context is not None:
|
||||
context.close()
|
||||
if stream:
|
||||
return self._stream_result(response)
|
||||
else:
|
||||
output = self._result(response)
|
||||
srch = r'Successfully built ([0-9a-f]+)'
|
||||
match = re.search(srch, output)
|
||||
if not match:
|
||||
return None, output
|
||||
return match.group(1), output
|
||||
|
||||
def commit(self, container, repository=None, tag=None, message=None,
|
||||
author=None, conf=None):
|
||||
params = {
|
||||
'container': container,
|
||||
'repo': repository,
|
||||
'tag': tag,
|
||||
'comment': message,
|
||||
'author': author
|
||||
}
|
||||
u = self._url("/commit")
|
||||
return self._result(self._post_json(u, data=conf, params=params),
|
||||
json=True)
|
||||
|
||||
def containers(self, quiet=False, all=False, trunc=True, latest=False,
|
||||
since=None, before=None, limit=-1):
|
||||
params = {
|
||||
'limit': 1 if latest else limit,
|
||||
'all': 1 if all else 0,
|
||||
'trunc_cmd': 1 if trunc else 0,
|
||||
'since': since,
|
||||
'before': before
|
||||
}
|
||||
u = self._url("/containers/json")
|
||||
res = self._result(self._get(u, params=params), True)
|
||||
|
||||
if quiet:
|
||||
return [{'Id': x['Id']} for x in res]
|
||||
return res
|
||||
|
||||
def copy(self, container, resource):
|
||||
res = self._post_json(
|
||||
self._url("/containers/{0}/copy".format(container)),
|
||||
data={"Resource": resource},
|
||||
stream=True
|
||||
)
|
||||
self._raise_for_status(res)
|
||||
return res.raw
|
||||
|
||||
def create_container(self, image, command=None, hostname=None, user=None,
|
||||
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, 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,
|
||||
entrypoint, cpu_shares, working_dir
|
||||
)
|
||||
return self.create_container_from_config(config, name)
|
||||
|
||||
def create_container_from_config(self, config, name=None):
|
||||
u = self._url("/containers/create")
|
||||
params = {
|
||||
'name': name
|
||||
}
|
||||
res = self._post_json(u, data=config, params=params)
|
||||
return self._result(res, True)
|
||||
|
||||
def diff(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
return self._result(self._get(self._url("/containers/{0}/changes".
|
||||
format(container))), True)
|
||||
|
||||
def events(self):
|
||||
u = self._url("/events")
|
||||
|
||||
socket = self._stream_result_socket(self.get(u, stream=True))
|
||||
|
||||
while True:
|
||||
chunk = socket.recv(4096)
|
||||
if chunk:
|
||||
# Messages come in the format of length, data, newline.
|
||||
length, data = chunk.split("\n", 1)
|
||||
length = int(length, 16)
|
||||
if length > len(data):
|
||||
data += socket.recv(length - len(data))
|
||||
yield json.loads(data)
|
||||
else:
|
||||
break
|
||||
|
||||
def export(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
res = self._get(self._url("/containers/{0}/export".format(container)),
|
||||
stream=True)
|
||||
self._raise_for_status(res)
|
||||
return res.raw
|
||||
|
||||
def history(self, image):
|
||||
res = self._get(self._url("/images/{0}/history".format(image)))
|
||||
self._raise_for_status(res)
|
||||
return self._result(res)
|
||||
|
||||
def images(self, name=None, quiet=False, all=False, viz=False):
|
||||
if viz:
|
||||
return self._result(self._get(self._url("images/viz")))
|
||||
params = {
|
||||
'filter': name,
|
||||
'only_ids': 1 if quiet else 0,
|
||||
'all': 1 if all else 0,
|
||||
}
|
||||
res = self._result(self._get(self._url("/images/json"), params=params),
|
||||
True)
|
||||
if quiet:
|
||||
return [x['Id'] for x in res]
|
||||
return res
|
||||
|
||||
def import_image(self, src=None, repository=None, tag=None, image=None):
|
||||
u = self._url("/images/create")
|
||||
params = {
|
||||
'repo': repository,
|
||||
'tag': tag
|
||||
}
|
||||
|
||||
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")),
|
||||
True)
|
||||
|
||||
def insert(self, image, url, path):
|
||||
api_url = self._url("/images/" + image + "/insert")
|
||||
params = {
|
||||
'url': url,
|
||||
'path': path
|
||||
}
|
||||
return self._result(self._post(api_url, params=params))
|
||||
|
||||
def inspect_container(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
return self._result(
|
||||
self._get(self._url("/containers/{0}/json".format(container))),
|
||||
True)
|
||||
|
||||
def inspect_image(self, image_id):
|
||||
return self._result(
|
||||
self._get(self._url("/images/{0}/json".format(image_id))),
|
||||
True
|
||||
)
|
||||
|
||||
def kill(self, container, signal=None):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
url = self._url("/containers/{0}/kill".format(container))
|
||||
params = {}
|
||||
if signal is not None:
|
||||
params['signal'] = signal
|
||||
res = self._post(url, params=params)
|
||||
|
||||
self._raise_for_status(res)
|
||||
|
||||
def login(self, username, password=None, email=None, registry=None,
|
||||
reauth=False):
|
||||
# If we don't have any auth data so far, try reloading the config file
|
||||
# one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
|
||||
registry = registry or auth.INDEX_URL
|
||||
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
# If we found an existing auth config for this registry and username
|
||||
# combination, we can return it immediately unless reauth is requested.
|
||||
if authcfg and authcfg.get('username', None) == username \
|
||||
and not reauth:
|
||||
return authcfg
|
||||
|
||||
req_data = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email,
|
||||
'serveraddress': registry,
|
||||
}
|
||||
|
||||
response = self._post_json(self._url('/auth'), data=req_data)
|
||||
if response.status_code == 200:
|
||||
self._auth_configs[registry] = req_data
|
||||
return self._result(response, json=True)
|
||||
|
||||
def logs(self, container, stdout=True, stderr=True, stream=False):
|
||||
return self.attach(
|
||||
container,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
stream=stream,
|
||||
logs=True
|
||||
)
|
||||
|
||||
def port(self, container, private_port):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
res = self._get(self._url("/containers/{0}/json".format(container)))
|
||||
self._raise_for_status(res)
|
||||
json_ = res.json()
|
||||
s_port = str(private_port)
|
||||
h_ports = None
|
||||
|
||||
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)
|
||||
if repo_name.count(":") == 1:
|
||||
repository, tag = repository.rsplit(":", 1)
|
||||
|
||||
params = {
|
||||
'tag': tag,
|
||||
'fromImage': repository
|
||||
}
|
||||
headers = {}
|
||||
|
||||
if utils.compare_version('1.5', self._version) >= 0:
|
||||
# If we don't have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
|
||||
# Do not fail here if no atuhentication exists for this specific
|
||||
# registry as we can have a readonly pull. Just put the header if
|
||||
# we can.
|
||||
if authcfg:
|
||||
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
|
||||
|
||||
response = self._post(self._url('/images/create'), params=params,
|
||||
headers=headers, stream=stream, timeout=None)
|
||||
|
||||
if stream:
|
||||
return self._stream_helper(response)
|
||||
else:
|
||||
return self._result(response)
|
||||
|
||||
def push(self, repository, stream=False):
|
||||
registry, repo_name = auth.resolve_repository_name(repository)
|
||||
u = self._url("/images/{0}/push".format(repository))
|
||||
headers = {}
|
||||
|
||||
if utils.compare_version('1.5', self._version) >= 0:
|
||||
# If we don't have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs:
|
||||
self._auth_configs = auth.load_config()
|
||||
authcfg = auth.resolve_authconfig(self._auth_configs, registry)
|
||||
|
||||
# Do not fail here if no atuhentication exists for this specific
|
||||
# registry as we can have a readonly pull. Just put the header if
|
||||
# we can.
|
||||
if authcfg:
|
||||
headers['X-Registry-Auth'] = auth.encode_header(authcfg)
|
||||
|
||||
response = self._post_json(u, None, headers=headers, stream=stream)
|
||||
else:
|
||||
response = self._post_json(u, authcfg, stream=stream)
|
||||
|
||||
return stream and self._stream_helper(response) \
|
||||
or self._result(response)
|
||||
|
||||
def remove_container(self, container, v=False, link=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'v': v, 'link': link}
|
||||
res = self._delete(self._url("/containers/" + container),
|
||||
params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def remove_image(self, image):
|
||||
res = self._delete(self._url("/images/" + image))
|
||||
self._raise_for_status(res)
|
||||
|
||||
def restart(self, container, timeout=10):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'t': timeout}
|
||||
url = self._url("/containers/{0}/restart".format(container))
|
||||
res = self._post(url, params=params)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def search(self, term):
|
||||
return self._result(self._get(self._url("/images/search"),
|
||||
params={'term': term}),
|
||||
True)
|
||||
|
||||
def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
|
||||
publish_all_ports=False, links=None, privileged=False):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
|
||||
if isinstance(lxc_conf, dict):
|
||||
formatted = []
|
||||
for k, v in six.iteritems(lxc_conf):
|
||||
formatted.append({'Key': k, 'Value': str(v)})
|
||||
lxc_conf = formatted
|
||||
|
||||
start_config = {
|
||||
'LxcConf': lxc_conf
|
||||
}
|
||||
if binds:
|
||||
bind_pairs = [
|
||||
'{0}:{1}'.format(host, dest) for host, dest in binds.items()
|
||||
]
|
||||
start_config['Binds'] = bind_pairs
|
||||
|
||||
if port_bindings:
|
||||
start_config['PortBindings'] = utils.convert_port_bindings(
|
||||
port_bindings
|
||||
)
|
||||
|
||||
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(links)
|
||||
]
|
||||
|
||||
start_config['Links'] = formatted_links
|
||||
|
||||
start_config['Privileged'] = privileged
|
||||
|
||||
url = self._url("/containers/{0}/start".format(container))
|
||||
res = self._post_json(url, data=start_config)
|
||||
self._raise_for_status(res)
|
||||
|
||||
def stop(self, container, timeout=10):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
params = {'t': timeout}
|
||||
url = self._url("/containers/{0}/stop".format(container))
|
||||
res = self._post(url, params=params,
|
||||
timeout=max(timeout, self._timeout))
|
||||
self._raise_for_status(res)
|
||||
|
||||
def tag(self, image, repository, tag=None, force=False):
|
||||
params = {
|
||||
'tag': tag,
|
||||
'repo': repository,
|
||||
'force': 1 if force else 0
|
||||
}
|
||||
url = self._url("/images/{0}/tag".format(image))
|
||||
res = self._post(url, params=params)
|
||||
self._raise_for_status(res)
|
||||
return res.status_code == 201
|
||||
|
||||
def top(self, container):
|
||||
u = self._url("/containers/{0}/top".format(container))
|
||||
return self._result(self._get(u), True)
|
||||
|
||||
def version(self):
|
||||
return self._result(self._get(self._url("/version")), True)
|
||||
|
||||
def wait(self, container):
|
||||
if isinstance(container, dict):
|
||||
container = container.get('Id')
|
||||
url = self._url("/containers/{0}/wait".format(container))
|
||||
res = self._post(url, timeout=None)
|
||||
self._raise_for_status(res)
|
||||
json_ = res.json()
|
||||
if 'StatusCode' in json_:
|
||||
return json_['StatusCode']
|
||||
return -1
|
||||
1
fig/packages/docker/unixconn/__init__.py
Normal file
1
fig/packages/docker/unixconn/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .unixconn import UnixAdapter # flake8: noqa
|
||||
71
fig/packages/docker/unixconn/unixconn.py
Normal file
71
fig/packages/docker/unixconn/unixconn.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import six
|
||||
|
||||
if six.PY3:
|
||||
import http.client as httplib
|
||||
else:
|
||||
import httplib
|
||||
import requests.adapters
|
||||
import socket
|
||||
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as connectionpool
|
||||
except ImportError:
|
||||
import urllib3.connectionpool as connectionpool
|
||||
|
||||
|
||||
class UnixHTTPConnection(httplib.HTTPConnection, object):
|
||||
def __init__(self, base_url, unix_socket, timeout=60):
|
||||
httplib.HTTPConnection.__init__(self, 'localhost', timeout=timeout)
|
||||
self.base_url = base_url
|
||||
self.unix_socket = unix_socket
|
||||
self.timeout = timeout
|
||||
|
||||
def connect(self):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(self.base_url.replace("http+unix:/", ""))
|
||||
self.sock = sock
|
||||
|
||||
def _extract_path(self, url):
|
||||
#remove the base_url entirely..
|
||||
return url.replace(self.base_url, "")
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
url = self._extract_path(self.unix_socket)
|
||||
super(UnixHTTPConnection, self).request(method, url, **kwargs)
|
||||
|
||||
|
||||
class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool):
|
||||
def __init__(self, base_url, socket_path, timeout=60):
|
||||
connectionpool.HTTPConnectionPool.__init__(self, 'localhost',
|
||||
timeout=timeout)
|
||||
self.base_url = base_url
|
||||
self.socket_path = socket_path
|
||||
self.timeout = timeout
|
||||
|
||||
def _new_conn(self):
|
||||
return UnixHTTPConnection(self.base_url, self.socket_path,
|
||||
self.timeout)
|
||||
|
||||
|
||||
class UnixAdapter(requests.adapters.HTTPAdapter):
|
||||
def __init__(self, base_url, timeout=60):
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
super(UnixAdapter, self).__init__()
|
||||
|
||||
def get_connection(self, socket_path, proxies=None):
|
||||
return UnixHTTPConnectionPool(self.base_url, socket_path, self.timeout)
|
||||
3
fig/packages/docker/utils/__init__.py
Normal file
3
fig/packages/docker/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .utils import (
|
||||
compare_version, convert_port_bindings, mkbuildcontext, ping, tar
|
||||
) # flake8: noqa
|
||||
96
fig/packages/docker/utils/utils.py
Normal file
96
fig/packages/docker/utils/utils.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Copyright 2013 dotCloud inc.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import io
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
|
||||
def mkbuildcontext(dockerfile):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w', fileobj=f)
|
||||
if isinstance(dockerfile, io.StringIO):
|
||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||
if six.PY3:
|
||||
raise TypeError('Please use io.BytesIO to create in-memory '
|
||||
'Dockerfiles with Python 3')
|
||||
else:
|
||||
dfinfo.size = len(dockerfile.getvalue())
|
||||
elif isinstance(dockerfile, io.BytesIO):
|
||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||
dfinfo.size = len(dockerfile.getvalue())
|
||||
else:
|
||||
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
|
||||
t.addfile(dfinfo, dockerfile)
|
||||
t.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
def tar(path):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w', fileobj=f)
|
||||
t.add(path, arcname='.')
|
||||
t.close()
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
|
||||
def compare_version(v1, v2):
|
||||
return float(v2) - float(v1)
|
||||
|
||||
|
||||
def ping(url):
|
||||
try:
|
||||
res = requests.get(url)
|
||||
return res.status >= 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _convert_port_binding(binding):
|
||||
result = {'HostIp': '', 'HostPort': ''}
|
||||
if isinstance(binding, tuple):
|
||||
if len(binding) == 2:
|
||||
result['HostPort'] = binding[1]
|
||||
result['HostIp'] = binding[0]
|
||||
elif isinstance(binding[0], six.string_types):
|
||||
result['HostIp'] = binding[0]
|
||||
else:
|
||||
result['HostPort'] = binding[0]
|
||||
else:
|
||||
result['HostPort'] = binding
|
||||
|
||||
if result['HostPort'] is None:
|
||||
result['HostPort'] = ''
|
||||
else:
|
||||
result['HostPort'] = str(result['HostPort'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_port_bindings(port_bindings):
|
||||
result = {}
|
||||
for k, v in six.iteritems(port_bindings):
|
||||
key = str(k)
|
||||
if '/' not in key:
|
||||
key = key + '/tcp'
|
||||
if isinstance(v, list):
|
||||
result[key] = [_convert_port_binding(binding) for binding in v]
|
||||
else:
|
||||
result[key] = [_convert_port_binding(v)]
|
||||
return result
|
||||
@@ -1,16 +1,37 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
from .service import Service
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_service_dicts(services):
|
||||
# Sort in dependency order
|
||||
def cmp(x, y):
|
||||
x_deps_y = y['name'] in x.get('links', [])
|
||||
y_deps_x = x['name'] in y.get('links', [])
|
||||
if x_deps_y and not y_deps_x:
|
||||
return 1
|
||||
elif y_deps_x and not x_deps_y:
|
||||
return -1
|
||||
return 0
|
||||
return sorted(services, cmp=cmp)
|
||||
# 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):
|
||||
"""
|
||||
@@ -40,7 +61,7 @@ class Project(object):
|
||||
@classmethod
|
||||
def from_config(cls, name, config, client):
|
||||
dicts = []
|
||||
for service_name, service in config.items():
|
||||
for service_name, service in list(config.items()):
|
||||
service['name'] = service_name
|
||||
dicts.append(service)
|
||||
return cls.from_dicts(name, dicts, client)
|
||||
@@ -73,21 +94,22 @@ class Project(object):
|
||||
unsorted = [self.get_service(name) for name in service_names]
|
||||
return [s for s in self.services if s in unsorted]
|
||||
|
||||
def create_containers(self, service_names=None):
|
||||
def recreate_containers(self, service_names=None):
|
||||
"""
|
||||
Returns a list of (service, container) tuples,
|
||||
one for each service with no running containers.
|
||||
For each service, create or recreate their containers.
|
||||
Returns a tuple with two lists. The first is a list of
|
||||
(service, old_container) tuples; the second is a list
|
||||
of (service, new_container) tuples.
|
||||
"""
|
||||
containers = []
|
||||
for service in self.get_services(service_names):
|
||||
if len(service.containers()) == 0:
|
||||
containers.append((service, service.create_container()))
|
||||
return containers
|
||||
old = []
|
||||
new = []
|
||||
|
||||
def kill_and_remove(self, tuples):
|
||||
for (service, container) in tuples:
|
||||
container.kill()
|
||||
container.remove()
|
||||
for service in self.get_services(service_names):
|
||||
(s_old, s_new) = service.recreate_containers()
|
||||
old += [(service, container) for container in s_old]
|
||||
new += [(service, container) for container in s_new]
|
||||
|
||||
return (old, new)
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
@@ -101,6 +123,13 @@ class Project(object):
|
||||
for service in self.get_services(service_names):
|
||||
service.kill(**options)
|
||||
|
||||
def build(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
if service.can_be_built():
|
||||
service.build(**options)
|
||||
else:
|
||||
log.info('%s uses an image, skipping' % service.name)
|
||||
|
||||
def remove_stopped(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.remove_stopped(**options)
|
||||
@@ -120,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
|
||||
153
fig/service.py
153
fig/service.py
@@ -1,4 +1,6 @@
|
||||
from docker.client import APIError
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from .packages.docker.client import APIError
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
@@ -12,6 +14,10 @@ class BuildError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CannotBeScaledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=[], **options):
|
||||
if not re.match('^[a-zA-Z0-9]+$', name):
|
||||
@@ -41,19 +47,57 @@ class Service(object):
|
||||
def start(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
if not c.is_running:
|
||||
log.info("Starting %s..." % c.name)
|
||||
self.start_container(c, **options)
|
||||
|
||||
def stop(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(**options)
|
||||
|
||||
def kill(self, **options):
|
||||
for c in self.containers():
|
||||
log.info("Killing %s..." % c.name)
|
||||
c.kill(**options)
|
||||
|
||||
def scale(self, desired_num):
|
||||
if not self.can_be_scaled():
|
||||
raise CannotBeScaledError()
|
||||
|
||||
# Create enough containers
|
||||
containers = self.containers(stopped=True)
|
||||
while len(containers) < desired_num:
|
||||
containers.append(self.create_container())
|
||||
|
||||
running_containers = []
|
||||
stopped_containers = []
|
||||
for c in containers:
|
||||
if c.is_running:
|
||||
running_containers.append(c)
|
||||
else:
|
||||
stopped_containers.append(c)
|
||||
running_containers.sort(key=lambda c: c.number)
|
||||
stopped_containers.sort(key=lambda c: c.number)
|
||||
|
||||
# Stop containers
|
||||
while len(running_containers) > desired_num:
|
||||
c = running_containers.pop()
|
||||
log.info("Stopping %s..." % c.name)
|
||||
c.stop(timeout=1)
|
||||
stopped_containers.append(c)
|
||||
|
||||
# Start containers
|
||||
while len(running_containers) < desired_num:
|
||||
c = stopped_containers.pop(0)
|
||||
log.info("Starting %s..." % c.name)
|
||||
c.start()
|
||||
running_containers.append(c)
|
||||
|
||||
|
||||
def remove_stopped(self, **options):
|
||||
for c in self.containers(stopped=True):
|
||||
if not c.is_running:
|
||||
log.info("Removing %s..." % c.name)
|
||||
c.remove(**options)
|
||||
|
||||
def create_container(self, one_off=False, **override_options):
|
||||
@@ -64,13 +108,56 @@ class Service(object):
|
||||
container_options = self._get_container_options(override_options, one_off=one_off)
|
||||
try:
|
||||
return Container.create(self.client, **container_options)
|
||||
except APIError, e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation:
|
||||
except APIError as e:
|
||||
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation):
|
||||
log.info('Pulling image %s...' % container_options['image'])
|
||||
self.client.pull(container_options['image'])
|
||||
return Container.create(self.client, **container_options)
|
||||
raise
|
||||
|
||||
def recreate_containers(self, **override_options):
|
||||
"""
|
||||
If a container for this service doesn't exist, create one. If there are
|
||||
any, stop them and create new ones. Does not remove the old containers.
|
||||
"""
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if len(containers) == 0:
|
||||
log.info("Creating %s..." % self.next_container_name())
|
||||
return ([], [self.create_container(**override_options)])
|
||||
else:
|
||||
old_containers = []
|
||||
new_containers = []
|
||||
|
||||
for c in containers:
|
||||
log.info("Recreating %s..." % c.name)
|
||||
(old_container, new_container) = self.recreate_container(c, **override_options)
|
||||
old_containers.append(old_container)
|
||||
new_containers.append(new_container)
|
||||
|
||||
return (old_containers, new_containers)
|
||||
|
||||
def recreate_container(self, container, **override_options):
|
||||
if container.is_running:
|
||||
container.stop(timeout=1)
|
||||
|
||||
intermediate_container = Container.create(
|
||||
self.client,
|
||||
image=container.image,
|
||||
command='echo',
|
||||
volumes_from=container.id,
|
||||
entrypoint=None
|
||||
)
|
||||
intermediate_container.start()
|
||||
intermediate_container.wait()
|
||||
container.remove()
|
||||
|
||||
options = dict(override_options)
|
||||
options['volumes_from'] = intermediate_container.id
|
||||
new_container = self.create_container(**options)
|
||||
|
||||
return (intermediate_container, new_container)
|
||||
|
||||
def start_container(self, container=None, **override_options):
|
||||
if container is None:
|
||||
container = self.create_container(**override_options)
|
||||
@@ -82,19 +169,21 @@ class Service(object):
|
||||
|
||||
if options.get('ports', None) is not None:
|
||||
for port in options['ports']:
|
||||
port = unicode(port)
|
||||
port = str(port)
|
||||
if ':' in port:
|
||||
internal_port, external_port = port.split(':', 1)
|
||||
port_bindings[int(internal_port)] = int(external_port)
|
||||
external_port, internal_port = port.split(':', 1)
|
||||
else:
|
||||
port_bindings[int(port)] = None
|
||||
external_port, internal_port = (None, port)
|
||||
|
||||
port_bindings[internal_port] = external_port
|
||||
|
||||
volume_bindings = {}
|
||||
|
||||
if options.get('volumes', None) is not None:
|
||||
for volume in options['volumes']:
|
||||
external_dir, internal_dir = volume.split(':')
|
||||
volume_bindings[os.path.abspath(external_dir)] = internal_dir
|
||||
if ':' in volume:
|
||||
external_dir, internal_dir = volume.split(':')
|
||||
volume_bindings[os.path.abspath(external_dir)] = internal_dir
|
||||
|
||||
container.start(
|
||||
links=self._get_links(),
|
||||
@@ -107,7 +196,7 @@ class Service(object):
|
||||
bits = [self.project, self.name]
|
||||
if one_off:
|
||||
bits.append('run')
|
||||
return '_'.join(bits + [unicode(self.next_container_number(one_off=one_off))])
|
||||
return '_'.join(bits + [str(self.next_container_number(one_off=one_off))])
|
||||
|
||||
def next_container_number(self, one_off=False):
|
||||
numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)]
|
||||
@@ -118,26 +207,38 @@ 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']
|
||||
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.update(override_options)
|
||||
|
||||
container_options['name'] = self.next_container_name(one_off)
|
||||
|
||||
if 'ports' in container_options:
|
||||
container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']]
|
||||
ports = []
|
||||
for port in container_options['ports']:
|
||||
port = str(port)
|
||||
if ':' in port:
|
||||
port = port.split(':')[-1]
|
||||
if '/' in port:
|
||||
port = tuple(port.split('/'))
|
||||
ports.append(port)
|
||||
container_options['ports'] = ports
|
||||
|
||||
if 'volumes' in container_options:
|
||||
container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes'])
|
||||
container_options['volumes'] = dict((split_volume(v)[1], {}) for v in container_options['volumes'])
|
||||
|
||||
if 'build' in self.options:
|
||||
if self.can_be_built():
|
||||
if len(self.client.images(name=self._build_tag_name())) == 0:
|
||||
self.build()
|
||||
container_options['image'] = self._build_tag_name()
|
||||
@@ -167,12 +268,21 @@ class Service(object):
|
||||
|
||||
return image_id
|
||||
|
||||
def can_be_built(self):
|
||||
return 'build' in self.options
|
||||
|
||||
def _build_tag_name(self):
|
||||
"""
|
||||
The tag to give to images built for this service.
|
||||
"""
|
||||
return '%s_%s' % (self.project, self.name)
|
||||
|
||||
def can_be_scaled(self):
|
||||
for port in self.options.get('ports', []):
|
||||
if ':' in str(port):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
|
||||
|
||||
@@ -203,3 +313,14 @@ def get_container_name(container):
|
||||
for name in container['Names']:
|
||||
if len(name.split('/')) == 2:
|
||||
return name[1:]
|
||||
|
||||
|
||||
def split_volume(v):
|
||||
"""
|
||||
If v is of the format EXTERNAL:INTERNAL, returns (EXTERNAL, INTERNAL).
|
||||
If v is of the format INTERNAL, returns (None, INTERNAL).
|
||||
"""
|
||||
if ':' in v:
|
||||
return v.split(':', 1)
|
||||
else:
|
||||
return (None, v)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
nose
|
||||
mock==1.0.1
|
||||
nose==1.3.0
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
docker-py==0.2.3
|
||||
docopt==0.6.1
|
||||
PyYAML==3.10
|
||||
requests==2.2.1
|
||||
six>=1.3.0
|
||||
texttable==0.8.1
|
||||
websocket-client==0.11.0
|
||||
|
||||
5
script/build-docs
Executable file
5
script/build-docs
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
pushd docs
|
||||
fig run jekyll jekyll build
|
||||
popd
|
||||
31
script/deploy-docs
Executable file
31
script/deploy-docs
Executable 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
2
script/open-docs
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
open file://`pwd`/docs/_site/index.html
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo 'pass a version as first argument'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git tag $1
|
||||
git push --tags
|
||||
python setup.py sdist upload
|
||||
|
||||
|
||||
23
script/travis
Executable file
23
script/travis
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on first error
|
||||
set -ex
|
||||
|
||||
# Put Python eggs in a writeable directory
|
||||
export PYTHON_EGG_CACHE="/tmp/.python-eggs"
|
||||
|
||||
# Activate correct virtualenv
|
||||
TRAVIS_PYTHON_VERSION=$1
|
||||
source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate
|
||||
|
||||
env
|
||||
|
||||
# Kill background processes on exit
|
||||
trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT
|
||||
|
||||
# Start docker daemon
|
||||
docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null &
|
||||
sleep 2
|
||||
|
||||
# $init is set by sekexe
|
||||
cd $(dirname $init)/.. && nosetests -v
|
||||
18
script/travis-install
Executable file
18
script/travis-install
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -"
|
||||
sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
|
||||
sudo apt-get update
|
||||
echo exit 101 | sudo tee /usr/sbin/policy-rc.d
|
||||
sudo chmod +x /usr/sbin/policy-rc.d
|
||||
sudo apt-get install -qy slirp lxc lxc-docker-0.7.5
|
||||
git clone git://github.com/jpetazzo/sekexe
|
||||
python setup.py install
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then
|
||||
pip install unittest2
|
||||
fi
|
||||
|
||||
24
setup.py
24
setup.py
@@ -1,16 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from setuptools import setup
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from setuptools import setup, find_packages
|
||||
import re
|
||||
import os
|
||||
import codecs
|
||||
|
||||
|
||||
# Borrowed from
|
||||
# https://github.com/jezdez/django_compressor/blob/develop/setup.py
|
||||
def read(*parts):
|
||||
return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read()
|
||||
path = os.path.join(os.path.dirname(__file__), *parts)
|
||||
with codecs.open(path, encoding='utf-8') as fobj:
|
||||
return fobj.read()
|
||||
|
||||
|
||||
def find_version(*file_paths):
|
||||
@@ -24,21 +25,22 @@ def find_version(*file_paths):
|
||||
with open('requirements.txt') as f:
|
||||
install_requires = f.read().splitlines()
|
||||
|
||||
with open('README.md') as f:
|
||||
long_description = f.read()
|
||||
with open('requirements-dev.txt') as f:
|
||||
tests_require = f.read().splitlines()
|
||||
|
||||
setup(
|
||||
name='fig',
|
||||
version=find_version("fig", "__init__.py"),
|
||||
description='Punctual, lightweight development environments using Docker',
|
||||
long_description=long_description,
|
||||
url='https://github.com/orchardup/fig',
|
||||
url='http://orchardup.github.io/fig/',
|
||||
author='Orchard Laboratories Ltd.',
|
||||
author_email='hello@orchardup.com',
|
||||
packages=['fig', 'fig.cli'],
|
||||
package_data={},
|
||||
license='BSD',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
test_suite='nose.collector',
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
fig=fig.cli.main:main
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (2,7):
|
||||
import unittest
|
||||
else:
|
||||
import unittest2 as unittest
|
||||
|
||||
|
||||
55
tests/cli_test.py
Normal file
55
tests/cli_test.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from .testcases import DockerClientTestCase
|
||||
from mock import patch
|
||||
from six import StringIO
|
||||
from fig.cli.main import TopLevelCommand
|
||||
|
||||
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))
|
||||
|
||||
@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
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=3', 'another=2']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 3)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 2)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 1)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 1)
|
||||
|
||||
self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']})
|
||||
self.assertEqual(len(project.get_service('simple').containers()), 0)
|
||||
self.assertEqual(len(project.get_service('another').containers()), 0)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from .testcases import DockerClientTestCase
|
||||
from fig.container import Container
|
||||
|
||||
@@ -34,3 +35,17 @@ class ContainerTest(DockerClientTestCase):
|
||||
'FOO': 'BAR',
|
||||
'BAZ': 'DOGE',
|
||||
})
|
||||
|
||||
def test_number(self):
|
||||
container = Container.from_ps(self.client, {
|
||||
"Id":"abc",
|
||||
"Image":"ubuntu:12.04",
|
||||
"Command":"sleep 300",
|
||||
"Created":1387384730,
|
||||
"Status":"Up 8 seconds",
|
||||
"Ports":None,
|
||||
"SizeRw":0,
|
||||
"SizeRootFs":0,
|
||||
"Names":["/db_1"]
|
||||
}, has_been_inspected=True)
|
||||
self.assertEqual(container.number, 1)
|
||||
|
||||
3
tests/fixtures/longer-filename-figfile/fig.yaml
vendored
Normal file
3
tests/fixtures/longer-filename-figfile/fig.yaml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
definedinyamlnotyml:
|
||||
image: ubuntu
|
||||
command: /bin/sleep 300
|
||||
6
tests/fixtures/simple-figfile/fig.yml
vendored
Normal file
6
tests/fixtures/simple-figfile/fig.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
simple:
|
||||
image: ubuntu
|
||||
command: /bin/sleep 300
|
||||
another:
|
||||
image: ubuntu
|
||||
command: /bin/sleep 300
|
||||
@@ -1,11 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
from fig.project import Project
|
||||
from fig.service import Service
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
class ProjectTest(DockerClientTestCase):
|
||||
def test_from_dict(self):
|
||||
project = Project.from_dicts('test', [
|
||||
project = Project.from_dicts('figtest', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'ubuntu'
|
||||
@@ -22,7 +22,7 @@ class ProjectTest(DockerClientTestCase):
|
||||
self.assertEqual(project.get_service('db').options['image'], 'ubuntu')
|
||||
|
||||
def test_from_dict_sorts_in_dependency_order(self):
|
||||
project = Project.from_dicts('test', [
|
||||
project = Project.from_dicts('figtest', [
|
||||
{
|
||||
'name': 'web',
|
||||
'image': 'ubuntu',
|
||||
@@ -42,58 +42,33 @@ class ProjectTest(DockerClientTestCase):
|
||||
project = Project('test', [web], self.client)
|
||||
self.assertEqual(project.get_service('web'), web)
|
||||
|
||||
def test_create_containers(self):
|
||||
def test_recreate_containers(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
project = Project('test', [web, db], self.client)
|
||||
|
||||
unstarted = project.create_containers(service_names=['web'])
|
||||
self.assertEqual(len(unstarted), 1)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
old_web_container = web.create_container()
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
unstarted = project.create_containers()
|
||||
self.assertEqual(len(unstarted), 2)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(unstarted[1][0], db)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 2)
|
||||
(old, new) = project.recreate_containers()
|
||||
self.assertEqual(len(old), 1)
|
||||
self.assertEqual(old[0][0], web)
|
||||
self.assertEqual(len(new), 2)
|
||||
self.assertEqual(new[0][0], web)
|
||||
self.assertEqual(new[1][0], db)
|
||||
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 1)
|
||||
|
||||
def test_up(self):
|
||||
web = self.create_service('web')
|
||||
db = self.create_service('db')
|
||||
other = self.create_service('other')
|
||||
project = Project('test', [web, db, other], self.client)
|
||||
|
||||
web.create_container()
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
unstarted = project.create_containers(service_names=['web', 'db'])
|
||||
self.assertEqual(len(unstarted), 2)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(unstarted[1][0], db)
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 2)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 1)
|
||||
|
||||
project.kill_and_remove(unstarted)
|
||||
|
||||
self.assertEqual(len(web.containers()), 0)
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
# 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')
|
||||
project = Project('test', [web, db], self.client)
|
||||
project = Project('figtest', [web, db], self.client)
|
||||
|
||||
project.start()
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from fig import Service
|
||||
from fig.service import CannotBeScaledError
|
||||
from .testcases import DockerClientTestCase
|
||||
|
||||
|
||||
@@ -29,7 +32,7 @@ class ServiceTest(DockerClientTestCase):
|
||||
foo.start_container()
|
||||
|
||||
self.assertEqual(len(foo.containers()), 1)
|
||||
self.assertEqual(foo.containers()[0].name, 'default_foo_1')
|
||||
self.assertEqual(foo.containers()[0].name, 'figtest_foo_1')
|
||||
self.assertEqual(len(bar.containers()), 0)
|
||||
|
||||
bar.start_container()
|
||||
@@ -39,8 +42,8 @@ class ServiceTest(DockerClientTestCase):
|
||||
self.assertEqual(len(bar.containers()), 2)
|
||||
|
||||
names = [c.name for c in bar.containers()]
|
||||
self.assertIn('default_bar_1', names)
|
||||
self.assertIn('default_bar_2', names)
|
||||
self.assertIn('figtest_bar_1', names)
|
||||
self.assertIn('figtest_bar_2', names)
|
||||
|
||||
def test_containers_one_off(self):
|
||||
db = self.create_service('db')
|
||||
@@ -49,9 +52,9 @@ class ServiceTest(DockerClientTestCase):
|
||||
self.assertEqual(db.containers(one_off=True, stopped=True), [container])
|
||||
|
||||
def test_project_is_added_to_container_name(self):
|
||||
service = self.create_service('web', project='myproject')
|
||||
service = self.create_service('web')
|
||||
service.start_container()
|
||||
self.assertEqual(service.containers()[0].name, 'myproject_web_1')
|
||||
self.assertEqual(service.containers()[0].name, 'figtest_web_1')
|
||||
|
||||
def test_start_stop(self):
|
||||
service = self.create_service('scalingtest')
|
||||
@@ -92,13 +95,48 @@ class ServiceTest(DockerClientTestCase):
|
||||
def test_create_container_with_one_off(self):
|
||||
db = self.create_service('db')
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'default_db_run_1')
|
||||
self.assertEqual(container.name, 'figtest_db_run_1')
|
||||
|
||||
def test_create_container_with_one_off_when_existing_container_is_running(self):
|
||||
db = self.create_service('db')
|
||||
db.start()
|
||||
container = db.create_container(one_off=True)
|
||||
self.assertEqual(container.name, 'default_db_run_1')
|
||||
self.assertEqual(container.name, 'figtest_db_run_1')
|
||||
|
||||
def test_create_container_with_unspecified_volume(self):
|
||||
service = self.create_service('db', volumes=['/var/db'])
|
||||
container = service.create_container()
|
||||
service.start_container(container)
|
||||
self.assertIn('/var/db', container.inspect()['Volumes'])
|
||||
|
||||
def test_recreate_containers(self):
|
||||
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps'])
|
||||
old_container = service.create_container()
|
||||
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']
|
||||
|
||||
num_containers_before = len(self.client.containers(all=True))
|
||||
|
||||
service.options['environment']['FOO'] = '2'
|
||||
(intermediate, new) = service.recreate_containers()
|
||||
self.assertEqual(len(intermediate), 1)
|
||||
self.assertEqual(len(new), 1)
|
||||
|
||||
new_container = new[0]
|
||||
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)
|
||||
|
||||
self.assertEqual(len(self.client.containers(all=True)), num_containers_before + 1)
|
||||
self.assertNotEqual(old_container.id, new_container.id)
|
||||
|
||||
def test_start_container_passes_through_options(self):
|
||||
db = self.create_service('db')
|
||||
@@ -115,27 +153,35 @@ class ServiceTest(DockerClientTestCase):
|
||||
web = self.create_service('web', links=[db])
|
||||
db.start_container()
|
||||
web.start_container()
|
||||
self.assertIn('default_db_1', web.containers()[0].links())
|
||||
db.stop(timeout=1)
|
||||
web.stop(timeout=1)
|
||||
self.assertIn('figtest_db_1', web.containers()[0].links())
|
||||
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(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='tests/fixtures/simple-dockerfile',
|
||||
project='figtest',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
self.assertIn('success', container.logs())
|
||||
self.assertEqual(len(self.client.images(name='default_test')), 1)
|
||||
self.assertEqual(len(self.client.images(name='figtest_test')), 1)
|
||||
|
||||
def test_start_container_uses_tagged_image_if_it_exists(self):
|
||||
self.client.build('tests/fixtures/simple-dockerfile', tag='default_test')
|
||||
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
|
||||
service = Service(
|
||||
name='test',
|
||||
client=self.client,
|
||||
build='this/does/not/exist/and/will/throw/error',
|
||||
project='figtest',
|
||||
)
|
||||
container = service.start_container()
|
||||
container.wait()
|
||||
@@ -144,13 +190,40 @@ class ServiceTest(DockerClientTestCase):
|
||||
def test_start_container_creates_ports(self):
|
||||
service = self.create_service('web', ports=[8000])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
|
||||
self.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()
|
||||
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
|
||||
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000')
|
||||
|
||||
def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self):
|
||||
service = self.create_service('web', ports=['8001:8000'])
|
||||
container = service.start_container().inspect()
|
||||
self.assertIn('8000/tcp', container['HostConfig']['PortBindings'])
|
||||
self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8001')
|
||||
|
||||
def test_scale(self):
|
||||
service = self.create_service('web')
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
service.scale(3)
|
||||
self.assertEqual(len(service.containers()), 3)
|
||||
service.scale(1)
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
service.scale(0)
|
||||
self.assertEqual(len(service.containers()), 0)
|
||||
|
||||
def test_scale_on_service_that_cannot_be_scaled(self):
|
||||
service = self.create_service('web', ports=['8000:8000'])
|
||||
self.assertRaises(CannotBeScaledError, lambda: service.scale(1))
|
||||
|
||||
|
||||
|
||||
|
||||
103
tests/sort_service_test.py
Normal file
103
tests/sort_service_test.py
Normal 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')
|
||||
37
tests/split_buffer_test.py
Normal file
37
tests/split_buffer_test.py
Normal 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"])
|
||||
@@ -1,25 +1,29 @@
|
||||
from docker import Client
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from fig.packages.docker import Client
|
||||
from fig.service import Service
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from fig.cli.utils import docker_url
|
||||
from . import unittest
|
||||
|
||||
|
||||
class DockerClientTestCase(TestCase):
|
||||
class DockerClientTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
cls.client = Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
cls.client = Client()
|
||||
cls.client.pull('ubuntu')
|
||||
cls.client = Client(docker_url())
|
||||
cls.client.pull('ubuntu', tag='latest')
|
||||
|
||||
def setUp(self):
|
||||
for c in self.client.containers(all=True):
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
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(
|
||||
project='figtest',
|
||||
name=name,
|
||||
client=self.client,
|
||||
image="ubuntu",
|
||||
|
||||
Reference in New Issue
Block a user