mirror of
https://github.com/docker/compose.git
synced 2026-02-12 19:49:22 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a21e3df4 | ||
|
|
45b7bd4361 | ||
|
|
36002f95ed | ||
|
|
0a92cbfa4d | ||
|
|
5b1fd64708 | ||
|
|
9b289b6f3b | ||
|
|
6d0702e607 | ||
|
|
38478ea504 | ||
|
|
a39db86651 | ||
|
|
853d8ad280 | ||
|
|
17c6ae067a | ||
|
|
770e78fdce | ||
|
|
e5065bed16 | ||
|
|
c0676e3fa3 | ||
|
|
d4f3ed1840 | ||
|
|
9ed6538693 | ||
|
|
ff65a3e1b0 | ||
|
|
ebf9bf387c | ||
|
|
fdc1e0f2e1 | ||
|
|
f3eff9a389 | ||
|
|
2857631e90 | ||
|
|
89cd7d8db0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
/dist
|
||||
/_site
|
||||
|
||||
18
CHANGES.md
Normal file
18
CHANGES.md
Normal file
@@ -0,0 +1,18 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
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.
|
||||
|
||||
|
||||
271
README.md
271
README.md
@@ -3,7 +3,7 @@ Fig
|
||||
|
||||
Punctual, lightweight development environments using Docker.
|
||||
|
||||
Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this:
|
||||
Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
@@ -16,148 +16,205 @@ db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
|
||||
Installing
|
||||
----------
|
||||
Then type `fig up`, and Fig will start and run your entire app:
|
||||
|
||||
```bash
|
||||
$ sudo pip install fig
|
||||
```
|
||||
$ fig up
|
||||
Pulling image orchardup/postgresql...
|
||||
Building web...
|
||||
Starting example_db_1...
|
||||
Starting example_web_1...
|
||||
example_db_1 | 2014-01-02 14:47:18 UTC LOG: database system is ready to accept connections
|
||||
example_web_1 | * Running on http://0.0.0.0:5000/
|
||||
|
||||
Defining your app
|
||||
-----------------
|
||||
There are commands to:
|
||||
|
||||
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.
|
||||
- start, stop and rebuild services
|
||||
- view the status of running services
|
||||
- tail running services' log output
|
||||
- run a one-off command on a service
|
||||
|
||||
The simplest way to get started is to just give it an image name:
|
||||
Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news.
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
```
|
||||
|
||||
You've now given Fig the minimal amount of configuration it needs to run:
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
```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...>
|
||||
```
|
||||
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.
|
||||
|
||||
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`.
|
||||
First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx):
|
||||
|
||||
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:
|
||||
$ curl https://raw.github.com/noplay/docker-osx/master/docker > /usr/local/bin/docker
|
||||
$ chmod +x /usr/local/bin/docker
|
||||
$ docker version
|
||||
|
||||
```bash
|
||||
$ fig up -d
|
||||
Starting myapp_db_1... done
|
||||
myapp_db_1 is running at 127.0.0.1:45678
|
||||
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.
|
||||
|
||||
$ fig ps
|
||||
Name State Ports
|
||||
------------------------------------
|
||||
myapp_db_1 Up 5432->45678/tcp
|
||||
```
|
||||
Next, install Fig:
|
||||
|
||||
### Building services
|
||||
$ sudo pip install fig
|
||||
|
||||
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).
|
||||
(If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.)
|
||||
|
||||
This example will build an image with `app.py` inside it:
|
||||
You'll want to make a directory for the project:
|
||||
|
||||
#### app.py
|
||||
$ 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
|
||||
print "Hello world!"
|
||||
from flask import Flask
|
||||
from redis import Redis
|
||||
import os
|
||||
app = Flask(__name__)
|
||||
redis = Redis(
|
||||
host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'),
|
||||
port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT'))
|
||||
)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
redis.incr('hits')
|
||||
return 'Hello World! I have been seen %s times.' % redis.get('hits')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", debug=True)
|
||||
```
|
||||
|
||||
#### fig.yml
|
||||
We define our Python dependencies in a file called `requirements.txt`:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
```
|
||||
flask
|
||||
redis
|
||||
|
||||
#### Dockerfile
|
||||
And we define how to build this into a Docker image using a file called `Dockerfile`:
|
||||
|
||||
FROM ubuntu:12.04
|
||||
RUN apt-get install python
|
||||
ADD . /opt
|
||||
WORKDIR /opt
|
||||
FROM stackbrew/ubuntu:13.10
|
||||
RUN apt-get -qq update
|
||||
RUN apt-get install -y python python-pip
|
||||
ADD . /code
|
||||
WORKDIR /code
|
||||
RUN pip install -r requirements.txt
|
||||
EXPOSE 5000
|
||||
CMD python app.py
|
||||
|
||||
That tells Docker to create an image with Python and Flask installed on it, run the command `python app.py`, and open port 5000 (the port that Flask listens on).
|
||||
|
||||
We then define a set of services using `fig.yml`:
|
||||
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- 5000:5000
|
||||
volumes:
|
||||
- .:/code
|
||||
links:
|
||||
- redis
|
||||
redis:
|
||||
image: orchardup/redis
|
||||
|
||||
This defines two services:
|
||||
|
||||
- `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image.
|
||||
- `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/).
|
||||
|
||||
Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up:
|
||||
|
||||
$ fig up
|
||||
Pulling image orchardup/redis...
|
||||
Building web...
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
|
||||
figtest_web_1 | * Running on http://0.0.0.0:5000/
|
||||
|
||||
Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running!
|
||||
|
||||
If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running:
|
||||
|
||||
$ fig up -d
|
||||
Starting figtest_redis_1...
|
||||
Starting figtest_web_1...
|
||||
$ fig ps
|
||||
Name Command State Ports
|
||||
-------------------------------------------------------------------
|
||||
figtest_redis_1 /usr/local/bin/run Up
|
||||
figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
|
||||
|
||||
`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service:
|
||||
|
||||
$ fig run web env
|
||||
|
||||
|
||||
### Getting your code in
|
||||
See `fig --help` other commands that are available.
|
||||
|
||||
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:
|
||||
You'll probably want to stop your services when you've finished with them:
|
||||
|
||||
$ fig stop
|
||||
|
||||
That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com).
|
||||
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
### fig.yml
|
||||
|
||||
Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts.
|
||||
|
||||
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
|
||||
|
||||
```yaml
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/opt
|
||||
-- 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
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
### Communicating between containers
|
||||
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.
|
||||
|
||||
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:
|
||||
<b><i>name</i>\_PORT</b><br>
|
||||
Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432`
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: orchardup/postgresql
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
|
||||
Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432`
|
||||
|
||||
web:
|
||||
build: .
|
||||
links:
|
||||
- db
|
||||
```
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
|
||||
Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5`
|
||||
|
||||
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:
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
|
||||
Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432`
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
Running a one-off command
|
||||
-------------------------
|
||||
|
||||
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
|
||||
```
|
||||
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
|
||||
Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp`
|
||||
|
||||
<b><i>name</i>\_NAME</b><br>
|
||||
Fully qualified container name, e.g. `MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1`
|
||||
|
||||
|
||||
[Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .service import Service
|
||||
|
||||
__version__ = '0.0.1'
|
||||
__version__ = '0.0.2'
|
||||
|
||||
@@ -7,17 +7,14 @@ 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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Command(DocoptCommand):
|
||||
@cached_property
|
||||
def client(self):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
return Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
return Client()
|
||||
return Client(docker_url())
|
||||
|
||||
@cached_property
|
||||
def project(self):
|
||||
|
||||
126
fig/cli/main.py
126
fig/cli/main.py
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import sys
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from inspect import getdoc
|
||||
|
||||
@@ -9,6 +11,7 @@ from ..project import NoSuchService
|
||||
from .command import Command
|
||||
from .formatter import Formatter
|
||||
from .log_printer import LogPrinter
|
||||
from .utils import yesno
|
||||
|
||||
from docker.client import APIError
|
||||
from .errors import UserError
|
||||
@@ -19,7 +22,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()
|
||||
@@ -70,14 +73,15 @@ class TopLevelCommand(Command):
|
||||
--version Print version and exit
|
||||
|
||||
Commands:
|
||||
up Create and start containers
|
||||
build Build or rebuild services
|
||||
kill Kill containers
|
||||
logs View output from containers
|
||||
ps List containers
|
||||
rm Remove stopped containers
|
||||
run Run a one-off command
|
||||
start Start services
|
||||
stop Stop services
|
||||
kill Kill containers
|
||||
rm Remove stopped containers
|
||||
up Create and start containers
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
@@ -85,6 +89,32 @@ class TopLevelCommand(Command):
|
||||
options['version'] = "fig %s" % __version__
|
||||
return options
|
||||
|
||||
def build(self, options):
|
||||
"""
|
||||
Build or rebuild services.
|
||||
|
||||
Usage: build [SERVICE...]
|
||||
"""
|
||||
self.project.build(service_names=options['SERVICE'])
|
||||
|
||||
def kill(self, options):
|
||||
"""
|
||||
Kill 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=False)
|
||||
print "Attaching to", list_containers(containers)
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
|
||||
def ps(self, options):
|
||||
"""
|
||||
List containers.
|
||||
@@ -116,6 +146,22 @@ class TopLevelCommand(Command):
|
||||
])
|
||||
print Formatter().table(headers, rows)
|
||||
|
||||
def rm(self, options):
|
||||
"""
|
||||
Remove stopped 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.
|
||||
@@ -145,36 +191,6 @@ class TopLevelCommand(Command):
|
||||
service.start_container(container, ports=None)
|
||||
c.run()
|
||||
|
||||
def up(self, options):
|
||||
"""
|
||||
Create and start containers.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new container names
|
||||
"""
|
||||
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:
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
self.project.kill_and_remove(unstarted)
|
||||
|
||||
def start(self, options):
|
||||
"""
|
||||
Start existing containers.
|
||||
@@ -191,31 +207,37 @@ class TopLevelCommand(Command):
|
||||
"""
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def kill(self, options):
|
||||
def up(self, options):
|
||||
"""
|
||||
Kill containers.
|
||||
Create and start containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
def rm(self, options):
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new container names
|
||||
"""
|
||||
Remove stopped containers
|
||||
detached = options['-d']
|
||||
|
||||
Usage: rm [SERVICE...]
|
||||
"""
|
||||
self.project.remove_stopped(service_names=options['SERVICE'])
|
||||
self.project.create_containers(service_names=options['SERVICE'])
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||
|
||||
def logs(self, options):
|
||||
"""
|
||||
View output from containers.
|
||||
if not detached:
|
||||
print "Attaching to", list_containers(containers)
|
||||
log_printer = LogPrinter(containers)
|
||||
|
||||
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()
|
||||
self.project.start(service_names=options['SERVICE'])
|
||||
|
||||
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)
|
||||
|
||||
print "Gracefully stopping... (press Ctrl+C again to force)"
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False):
|
||||
stdio = self.client.attach_socket(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import datetime
|
||||
import os
|
||||
import socket
|
||||
from .errors import UserError
|
||||
|
||||
|
||||
def cached_property(f):
|
||||
@@ -74,3 +76,36 @@ def mkdir(path, permissions=0700):
|
||||
os.chmod(path, permissions)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def docker_url():
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
return os.environ['DOCKER_URL']
|
||||
|
||||
socket_path = '/var/run/docker.sock'
|
||||
tcp_hosts = [
|
||||
('localdocker', 4243),
|
||||
('127.0.0.1', 4243),
|
||||
]
|
||||
tcp_host = '127.0.0.1'
|
||||
tcp_port = 4243
|
||||
|
||||
if os.path.exists(socket_path):
|
||||
return 'unix://%s' % socket_path
|
||||
|
||||
for host, port in tcp_hosts:
|
||||
try:
|
||||
s = socket.create_connection((host, port), timeout=1)
|
||||
s.close()
|
||||
return 'http://%s:%s' % (host, port)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise UserError("""
|
||||
Couldn't find Docker daemon - tried:
|
||||
|
||||
unix://%s
|
||||
%s
|
||||
|
||||
If it's running elsewhere, specify a url with DOCKER_URL.
|
||||
""" % (socket_path, '\n'.join('tcp://%s:%s' % h for h in tcp_hosts)))
|
||||
|
||||
@@ -75,19 +75,11 @@ class Project(object):
|
||||
|
||||
def create_containers(self, service_names=None):
|
||||
"""
|
||||
Returns a list of (service, container) tuples,
|
||||
one for each service with no running containers.
|
||||
For each service, creates a container if there are none.
|
||||
"""
|
||||
containers = []
|
||||
for service in self.get_services(service_names):
|
||||
if len(service.containers()) == 0:
|
||||
containers.append((service, service.create_container()))
|
||||
return containers
|
||||
|
||||
def kill_and_remove(self, tuples):
|
||||
for (service, container) in tuples:
|
||||
container.kill()
|
||||
container.remove()
|
||||
if len(service.containers(stopped=True)) == 0:
|
||||
service.create_container()
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
@@ -101,6 +93,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')
|
||||
|
||||
def remove_stopped(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
service.remove_stopped(**options)
|
||||
|
||||
@@ -137,7 +137,7 @@ class Service(object):
|
||||
if 'volumes' in container_options:
|
||||
container_options['volumes'] = dict((v.split(':')[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,6 +167,9 @@ 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.
|
||||
|
||||
4
setup.py
4
setup.py
@@ -24,14 +24,10 @@ 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()
|
||||
|
||||
setup(
|
||||
name='fig',
|
||||
version=find_version("fig", "__init__.py"),
|
||||
description='Punctual, lightweight development environments using Docker',
|
||||
long_description=long_description,
|
||||
url='https://github.com/orchardup/fig',
|
||||
author='Orchard Laboratories Ltd.',
|
||||
author_email='hello@orchardup.com',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 +21,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',
|
||||
@@ -47,53 +46,18 @@ class ProjectTest(DockerClientTestCase):
|
||||
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)
|
||||
project.create_containers(service_names=['web'])
|
||||
self.assertEqual(len(web.containers(stopped=True)), 1)
|
||||
self.assertEqual(len(db.containers(stopped=True)), 0)
|
||||
|
||||
unstarted = project.create_containers()
|
||||
self.assertEqual(len(unstarted), 2)
|
||||
self.assertEqual(unstarted[0][0], web)
|
||||
self.assertEqual(unstarted[1][0], db)
|
||||
self.assertEqual(len(web.containers(stopped=True)), 2)
|
||||
project.create_containers()
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -29,7 +29,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 +39,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 +49,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 +92,13 @@ 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_start_container_passes_through_options(self):
|
||||
db = self.create_service('db')
|
||||
@@ -115,7 +115,7 @@ 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())
|
||||
self.assertIn('figtest_db_1', web.containers()[0].links())
|
||||
db.stop(timeout=1)
|
||||
web.stop(timeout=1)
|
||||
|
||||
@@ -124,18 +124,20 @@ class ServiceTest(DockerClientTestCase):
|
||||
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()
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
from docker import Client
|
||||
from fig.service import Service
|
||||
import os
|
||||
from fig.cli.utils import docker_url
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class DockerClientTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
cls.client = Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
cls.client = Client()
|
||||
cls.client = Client(docker_url())
|
||||
cls.client.pull('ubuntu')
|
||||
|
||||
def setUp(self):
|
||||
for c in self.client.containers(all=True):
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
if c['Names'] and 'figtest' in c['Names'][0]:
|
||||
self.client.kill(c['Id'])
|
||||
self.client.remove_container(c['Id'])
|
||||
|
||||
def create_service(self, name, **kwargs):
|
||||
return Service(
|
||||
project='figtest',
|
||||
name=name,
|
||||
client=self.client,
|
||||
image="ubuntu",
|
||||
|
||||
Reference in New Issue
Block a user