mirror of
https://github.com/docker/compose.git
synced 2026-02-17 05:52:33 +08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f4b16181d | ||
|
|
5dde2a2498 | ||
|
|
06a1b32c12 | ||
|
|
46433c70b6 | ||
|
|
9260603149 | ||
|
|
8773f51583 | ||
|
|
38a3ee8d63 | ||
|
|
cb275b8633 | ||
|
|
604b370bb4 | ||
|
|
782a46fd60 | ||
|
|
1bd3d0dd77 | ||
|
|
b64ea85916 | ||
|
|
b47ab2b0f6 | ||
|
|
11280e4f30 | ||
|
|
a83876da09 | ||
|
|
e66c0452d5 | ||
|
|
899670fc6c | ||
|
|
ea45715a50 | ||
|
|
28f9c8d047 | ||
|
|
292fe6640e | ||
|
|
b759b9854a | ||
|
|
40943d5c81 | ||
|
|
93a0195dc8 | ||
|
|
7f0745d146 | ||
|
|
9813a8d5be | ||
|
|
491181ec31 | ||
|
|
e1ad5b1b99 | ||
|
|
67d4b7c587 | ||
|
|
98f663bab2 | ||
|
|
75b3dcf5fd | ||
|
|
deeb6c2236 | ||
|
|
e1e2b75691 | ||
|
|
392c118bbc | ||
|
|
dbd1e56dd3 | ||
|
|
7544580b4b | ||
|
|
2efb4f5be0 | ||
|
|
5e8dc6c972 | ||
|
|
23c8ec8930 | ||
|
|
bd8affb7aa | ||
|
|
7a943739cb | ||
|
|
d3f88cace5 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.git
|
||||
11
CHANGES.md
11
CHANGES.md
@@ -1,7 +1,14 @@
|
||||
Change log
|
||||
==========
|
||||
|
||||
1.0.0 (2014-10-07)
|
||||
1.0.1 (2014-11-04)
|
||||
------------------
|
||||
|
||||
- Added an `--allow-insecure-ssl` option to allow `fig up`, `fig run` and `fig pull` to pull from insecure registries.
|
||||
- Fixed `fig run` not showing output in Jenkins.
|
||||
- Fixed a bug where Fig couldn't build Dockerfiles with ADD statements pointing at URLs.
|
||||
|
||||
1.0.0 (2014-10-16)
|
||||
------------------
|
||||
|
||||
The highlights:
|
||||
@@ -43,6 +50,8 @@ Other things:
|
||||
- When starting a service where `volumes_from` points to a service without any containers running, that service will now be started.
|
||||
- Lots of docs improvements. Notably, environment variables are documented and official repositories are used throughout.
|
||||
|
||||
Thanks @dnephin, @d11wtq, @marksteve, @rubbish, @jbalonso, @timfreund, @alunduil, @mieciu, @shuron, @moss, @suzaku and @chmouel! Whew.
|
||||
|
||||
0.5.2 (2014-07-28)
|
||||
------------------
|
||||
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
# Contributing to Fig
|
||||
|
||||
## TL;DR
|
||||
|
||||
Pull requests will need:
|
||||
|
||||
- Tests
|
||||
- Documentation
|
||||
- [To be signed off](#sign-your-work)
|
||||
- A logical series of [well written commits](https://github.com/alphagov/styleguides/blob/master/git.md)
|
||||
|
||||
## Development environment
|
||||
|
||||
If you're looking contribute to [Fig](http://www.fig.sh/)
|
||||
but you're new to the project or maybe even to Python, here are the steps
|
||||
that should get you started.
|
||||
|
||||
1. Fork [https://github.com/docker/fig](https://github.com/docker/fig) to your username. kvz in this example.
|
||||
1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
|
||||
1. Fork [https://github.com/docker/fig](https://github.com/docker/fig) to your username.
|
||||
1. Clone your forked repository locally `git clone git@github.com:yourusername/fig.git`.
|
||||
1. Enter the local directory `cd fig`.
|
||||
1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
|
||||
1. Works for you? Run the test suite via `./script/test` to verify it won't break other usecases.
|
||||
1. All good? Commit and push to GitHub, and submit a pull request.
|
||||
1. Set up a development environment by running `python setup.py develop`. This will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repository. When you now run `fig` from anywhere on your machine, it will run your development version of Fig.
|
||||
|
||||
## Running the test suite
|
||||
|
||||
$ script/test
|
||||
|
||||
## Building binaries
|
||||
|
||||
Linux:
|
||||
|
||||
$ script/build-linux
|
||||
|
||||
OS X:
|
||||
|
||||
$ script/build-osx
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
|
||||
## Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the
|
||||
@@ -73,6 +68,17 @@ The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
|
||||
|
||||
$ git commit --signoff
|
||||
|
||||
## Building binaries
|
||||
|
||||
Linux:
|
||||
|
||||
$ script/build-linux
|
||||
|
||||
OS X:
|
||||
|
||||
$ script/build-osx
|
||||
|
||||
Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
|
||||
|
||||
## Release process
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
|
||||
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
|
||||
Chris Corbyn <chris@w3style.co.uk> (@d11wtq)
|
||||
Daniel Nephin <dnephin@gmail.com> (@dnephin)
|
||||
Nathan LeClaire <nathan.leclaire@gmail.com> (@nathanleclaire)
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<div class="badges">
|
||||
<iframe src="http://ghbtns.com/github-btn.html?user=docker&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>
|
||||
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://www.fig.sh/">Tweet</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
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('create', 'UA-43996733-3', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
</script>
|
||||
|
||||
18
docs/cli.md
18
docs/cli.md
@@ -10,6 +10,24 @@ Most commands are run against one or more services. If the service is omitted, i
|
||||
|
||||
Run `fig [COMMAND] --help` for full usage.
|
||||
|
||||
## Options
|
||||
|
||||
### --verbose
|
||||
|
||||
Show more output
|
||||
|
||||
### --version
|
||||
|
||||
Print version and exit
|
||||
|
||||
### -f, --file FILE
|
||||
|
||||
Specify an alternate fig file (default: fig.yml)
|
||||
|
||||
### -p, --project-name NAME
|
||||
|
||||
Specify an alternate project name (default: directory name)
|
||||
|
||||
## Commands
|
||||
|
||||
### build
|
||||
|
||||
@@ -5,14 +5,14 @@ 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:
|
||||
Define your app's environment with a `Dockerfile` so it can be reproduced anywhere:
|
||||
|
||||
FROM 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:
|
||||
Define the services that make up your app in `fig.yml` so they can be run together in an isolated environment:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
|
||||
@@ -18,7 +18,7 @@ There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntuli
|
||||
|
||||
Next, install Fig:
|
||||
|
||||
curl -L https://github.com/docker/fig/releases/download/1.0.0/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig
|
||||
curl -L https://github.com/docker/fig/releases/download/1.0.1/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig
|
||||
|
||||
Releases are available for OS X and 64-bit Linux. Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Getting started with Fig and Wordpress
|
||||
|
||||
Fig makes it nice and easy to run Wordpress in an isolated environment. [Install Fig](install.html), then download Wordpress into the current directory:
|
||||
|
||||
$ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
|
||||
$ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
|
||||
|
||||
This will create a directory called `wordpress`, which you can rename to the name of your project if you wish. Inside that directory, we create `Dockerfile`, a file that defines what environment your app is going to run in:
|
||||
|
||||
@@ -37,7 +37,7 @@ db:
|
||||
MYSQL_DATABASE: wordpress
|
||||
```
|
||||
|
||||
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to make it read the MySQL host and port from the environment variables passed in by Fig:
|
||||
Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to point the database configuration at the `db` container:
|
||||
|
||||
```
|
||||
<?php
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals
|
||||
from .service import Service # noqa:flake8
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '1.0.1'
|
||||
|
||||
@@ -42,6 +42,11 @@ class Command(DocoptCommand):
|
||||
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
|
||||
|
||||
def perform_command(self, options, handler, command_options):
|
||||
if options['COMMAND'] == 'help':
|
||||
# Skip looking up the figfile.
|
||||
handler(None, command_options)
|
||||
return
|
||||
|
||||
explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE')
|
||||
project = self.get_project(
|
||||
self.get_config_path(explicit_config_path),
|
||||
|
||||
@@ -11,7 +11,7 @@ def docker_client():
|
||||
"""
|
||||
cert_path = os.environ.get('DOCKER_CERT_PATH', '')
|
||||
if cert_path == '':
|
||||
cert_path = os.path.join(os.environ.get('HOME'), '.docker')
|
||||
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
|
||||
|
||||
base_url = os.environ.get('DOCKER_HOST')
|
||||
tls_config = None
|
||||
|
||||
@@ -7,7 +7,7 @@ import signal
|
||||
from operator import attrgetter
|
||||
|
||||
from inspect import getdoc
|
||||
from fig.packages import dockerpty
|
||||
import dockerpty
|
||||
|
||||
from .. import __version__
|
||||
from ..project import NoSuchService, ConfigurationError
|
||||
@@ -68,7 +68,7 @@ def parse_doc_section(name, source):
|
||||
|
||||
|
||||
class TopLevelCommand(Command):
|
||||
"""Punctual, lightweight development environments using Docker.
|
||||
"""Fast, isolated development environments using Docker.
|
||||
|
||||
Usage:
|
||||
fig [options] [COMMAND] [ARGS...]
|
||||
@@ -264,17 +264,21 @@ class TopLevelCommand(Command):
|
||||
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run container in the background, print
|
||||
new container name.
|
||||
--entrypoint CMD Override the entrypoint of the image.
|
||||
-e KEY=VAL Set an environment variable (can be used multiple times)
|
||||
--no-deps Don't start linked services.
|
||||
--rm Remove container after run. Ignored in detached mode.
|
||||
-T Disable pseudo-tty allocation. By default `fig run`
|
||||
allocates a TTY.
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
-d Detached mode: Run container in the background, print
|
||||
new container name.
|
||||
--entrypoint CMD Override the entrypoint of the image.
|
||||
-e KEY=VAL Set an environment variable (can be used multiple times)
|
||||
--no-deps Don't start linked services.
|
||||
--rm Remove container after run. Ignored in detached mode.
|
||||
-T Disable pseudo-tty allocation. By default `fig run`
|
||||
allocates a TTY.
|
||||
"""
|
||||
service = project.get_service(options['SERVICE'])
|
||||
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_names()
|
||||
|
||||
@@ -309,14 +313,17 @@ class TopLevelCommand(Command):
|
||||
|
||||
if options['--entrypoint']:
|
||||
container_options['entrypoint'] = options.get('--entrypoint')
|
||||
|
||||
container = service.create_container(one_off=True, **container_options)
|
||||
container = service.create_container(
|
||||
one_off=True,
|
||||
insecure_registry=insecure_registry,
|
||||
**container_options
|
||||
)
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
print(container.name)
|
||||
else:
|
||||
service.start_container(container, ports=None, one_off=True)
|
||||
dockerpty.start(project.client, container.id)
|
||||
dockerpty.start(project.client, container.id, interactive=not options['-T'])
|
||||
exit_code = container.wait()
|
||||
if options['--rm']:
|
||||
log.info("Removing %s..." % container.name)
|
||||
@@ -396,12 +403,15 @@ class TopLevelCommand(Command):
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background,
|
||||
print new container names.
|
||||
--no-color Produce monochrome output.
|
||||
--no-deps Don't start linked services.
|
||||
--no-recreate If containers already exist, don't recreate them.
|
||||
--allow-insecure-ssl Allow insecure connections to the docker
|
||||
registry
|
||||
-d Detached mode: Run containers in the background,
|
||||
print new container names.
|
||||
--no-color Produce monochrome output.
|
||||
--no-deps Don't start linked services.
|
||||
--no-recreate If containers already exist, don't recreate them.
|
||||
"""
|
||||
insecure_registry = options['--allow-insecure-ssl']
|
||||
detached = options['-d']
|
||||
|
||||
monochrome = options['--no-color']
|
||||
@@ -413,7 +423,8 @@ class TopLevelCommand(Command):
|
||||
project.up(
|
||||
service_names=service_names,
|
||||
start_links=start_links,
|
||||
recreate=recreate
|
||||
recreate=recreate,
|
||||
insecure_registry=insecure_registry,
|
||||
)
|
||||
|
||||
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# dockerpty.
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 .pty import PseudoTerminal
|
||||
|
||||
|
||||
def start(client, container):
|
||||
"""
|
||||
Present the PTY of the container inside the current process.
|
||||
|
||||
This is just a wrapper for PseudoTerminal(client, container).start()
|
||||
"""
|
||||
|
||||
PseudoTerminal(client, container).start()
|
||||
@@ -1,294 +0,0 @@
|
||||
# dockerpty: io.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 os
|
||||
import fcntl
|
||||
import errno
|
||||
import struct
|
||||
import select as builtin_select
|
||||
|
||||
|
||||
def set_blocking(fd, blocking=True):
|
||||
"""
|
||||
Set the given file-descriptor blocking or non-blocking.
|
||||
|
||||
Returns the original blocking status.
|
||||
"""
|
||||
|
||||
old_flag = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
|
||||
if blocking:
|
||||
new_flag = old_flag &~ os.O_NONBLOCK
|
||||
else:
|
||||
new_flag = old_flag | os.O_NONBLOCK
|
||||
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, new_flag)
|
||||
|
||||
return not bool(old_flag & os.O_NONBLOCK)
|
||||
|
||||
|
||||
def select(read_streams, timeout=0):
|
||||
"""
|
||||
Select the streams from `read_streams` that are ready for reading.
|
||||
|
||||
Uses `select.select()` internally but returns a flat list of streams.
|
||||
"""
|
||||
|
||||
write_streams = []
|
||||
exception_streams = []
|
||||
|
||||
try:
|
||||
return builtin_select.select(
|
||||
read_streams,
|
||||
write_streams,
|
||||
exception_streams,
|
||||
timeout,
|
||||
)[0]
|
||||
except builtin_select.error as e:
|
||||
# POSIX signals interrupt select()
|
||||
if e[0] == errno.EINTR:
|
||||
return []
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
class Stream(object):
|
||||
"""
|
||||
Generic Stream class.
|
||||
|
||||
This is a file-like abstraction on top of os.read() and os.write(), which
|
||||
add consistency to the reading of sockets and files alike.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Recoverable IO/OS Errors.
|
||||
"""
|
||||
ERRNO_RECOVERABLE = [
|
||||
errno.EINTR,
|
||||
errno.EDEADLK,
|
||||
errno.EWOULDBLOCK,
|
||||
]
|
||||
|
||||
|
||||
def __init__(self, fd):
|
||||
"""
|
||||
Initialize the Stream for the file descriptor `fd`.
|
||||
|
||||
The `fd` object must have a `fileno()` method.
|
||||
"""
|
||||
self.fd = fd
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Return the fileno() of the file descriptor.
|
||||
"""
|
||||
|
||||
return self.fd.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
if hasattr(self.fd, 'setblocking'):
|
||||
self.fd.setblocking(value)
|
||||
return True
|
||||
else:
|
||||
return set_blocking(self.fd, value)
|
||||
|
||||
|
||||
def read(self, n=4096):
|
||||
"""
|
||||
Return `n` bytes of data from the Stream, or None at end of stream.
|
||||
"""
|
||||
|
||||
try:
|
||||
if hasattr(self.fd, 'recv'):
|
||||
return self.fd.recv(n)
|
||||
return os.read(self.fd.fileno(), n)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in Stream.ERRNO_RECOVERABLE:
|
||||
raise e
|
||||
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write `data` to the Stream.
|
||||
"""
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
while True:
|
||||
try:
|
||||
if hasattr(self.fd, 'send'):
|
||||
self.fd.send(data)
|
||||
return len(data)
|
||||
os.write(self.fd.fileno(), data)
|
||||
return len(data)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in Stream.ERRNO_RECOVERABLE:
|
||||
raise e
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({fd})".format(cls=type(self).__name__, fd=self.fd)
|
||||
|
||||
|
||||
class Demuxer(object):
|
||||
"""
|
||||
Wraps a multiplexed Stream to read in data demultiplexed.
|
||||
|
||||
Docker multiplexes streams together when there is no PTY attached, by
|
||||
sending an 8-byte header, followed by a chunk of data.
|
||||
|
||||
The first 4 bytes of the header denote the stream from which the data came
|
||||
(i.e. 0x01 = stdout, 0x02 = stderr). Only the first byte of these initial 4
|
||||
bytes is used.
|
||||
|
||||
The next 4 bytes indicate the length of the following chunk of data as an
|
||||
integer in big endian format. This much data must be consumed before the
|
||||
next 8-byte header is read.
|
||||
"""
|
||||
|
||||
def __init__(self, stream):
|
||||
"""
|
||||
Initialize a new Demuxer reading from `stream`.
|
||||
"""
|
||||
|
||||
self.stream = stream
|
||||
self.remain = 0
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns the fileno() of the underlying Stream.
|
||||
|
||||
This is useful for select() to work.
|
||||
"""
|
||||
|
||||
return self.stream.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
return self.stream.set_blocking(value)
|
||||
|
||||
|
||||
def read(self, n=4096):
|
||||
"""
|
||||
Read up to `n` bytes of data from the Stream, after demuxing.
|
||||
|
||||
Less than `n` bytes of data may be returned depending on the available
|
||||
payload, but the number of bytes returned will never exceed `n`.
|
||||
|
||||
Because demuxing involves scanning 8-byte headers, the actual amount of
|
||||
data read from the underlying stream may be greater than `n`.
|
||||
"""
|
||||
|
||||
size = self._next_packet_size(n)
|
||||
|
||||
if size <= 0:
|
||||
return
|
||||
else:
|
||||
return self.stream.read(size)
|
||||
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Delegates the the underlying Stream.
|
||||
"""
|
||||
|
||||
return self.stream.write(data)
|
||||
|
||||
|
||||
def _next_packet_size(self, n=0):
|
||||
size = 0
|
||||
|
||||
if self.remain > 0:
|
||||
size = min(n, self.remain)
|
||||
self.remain -= size
|
||||
else:
|
||||
data = self.stream.read(8)
|
||||
if data is None:
|
||||
return 0
|
||||
if len(data) == 8:
|
||||
__, actual = struct.unpack('>BxxxL', data)
|
||||
size = min(n, actual)
|
||||
self.remain = actual - size
|
||||
|
||||
return size
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({stream})".format(cls=type(self).__name__,
|
||||
stream=self.stream)
|
||||
|
||||
|
||||
class Pump(object):
|
||||
"""
|
||||
Stream pump class.
|
||||
|
||||
A Pump wraps two Streams, reading from one and and writing its data into
|
||||
the other, much like a pipe but manually managed.
|
||||
|
||||
This abstraction is used to facilitate piping data between the file
|
||||
descriptors associated with the tty and those associated with a container's
|
||||
allocated pty.
|
||||
|
||||
Pumps are selectable based on the 'read' end of the pipe.
|
||||
"""
|
||||
|
||||
def __init__(self, from_stream, to_stream):
|
||||
"""
|
||||
Initialize a Pump with a Stream to read from and another to write to.
|
||||
"""
|
||||
|
||||
self.from_stream = from_stream
|
||||
self.to_stream = to_stream
|
||||
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
Returns the `fileno()` of the reader end of the Pump.
|
||||
|
||||
This is useful to allow Pumps to function with `select()`.
|
||||
"""
|
||||
|
||||
return self.from_stream.fileno()
|
||||
|
||||
|
||||
def set_blocking(self, value):
|
||||
return self.from_stream.set_blocking(value)
|
||||
|
||||
|
||||
def flush(self, n=4096):
|
||||
"""
|
||||
Flush `n` bytes of data from the reader Stream to the writer Stream.
|
||||
|
||||
Returns the number of bytes that were actually flushed. A return value
|
||||
of zero is not an error.
|
||||
|
||||
If EOF has been reached, `None` is returned.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.to_stream.write(self.from_stream.read(n))
|
||||
except OSError as e:
|
||||
if e.errno != errno.EPIPE:
|
||||
raise e
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}(from={from_stream}, to={to_stream})".format(
|
||||
cls=type(self).__name__,
|
||||
from_stream=self.from_stream,
|
||||
to_stream=self.to_stream)
|
||||
@@ -1,235 +0,0 @@
|
||||
# dockerpty: pty.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 sys
|
||||
import signal
|
||||
from ssl import SSLError
|
||||
|
||||
from . import io
|
||||
from . import tty
|
||||
|
||||
|
||||
class WINCHHandler(object):
|
||||
"""
|
||||
WINCH Signal handler to keep the PTY correctly sized.
|
||||
"""
|
||||
|
||||
def __init__(self, pty):
|
||||
"""
|
||||
Initialize a new WINCH handler for the given PTY.
|
||||
|
||||
Initializing a handler has no immediate side-effects. The `start()`
|
||||
method must be invoked for the signals to be trapped.
|
||||
"""
|
||||
|
||||
self.pty = pty
|
||||
self.original_handler = None
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Invoked on entering a `with` block.
|
||||
"""
|
||||
|
||||
self.start()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""
|
||||
Invoked on exiting a `with` block.
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start trapping WINCH signals and resizing the PTY.
|
||||
|
||||
This method saves the previous WINCH handler so it can be restored on
|
||||
`stop()`.
|
||||
"""
|
||||
|
||||
def handle(signum, frame):
|
||||
if signum == signal.SIGWINCH:
|
||||
self.pty.resize()
|
||||
|
||||
self.original_handler = signal.signal(signal.SIGWINCH, handle)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop trapping WINCH signals and restore the previous WINCH handler.
|
||||
"""
|
||||
|
||||
if self.original_handler is not None:
|
||||
signal.signal(signal.SIGWINCH, self.original_handler)
|
||||
|
||||
|
||||
class PseudoTerminal(object):
|
||||
"""
|
||||
Wraps the pseudo-TTY (PTY) allocated to a docker container.
|
||||
|
||||
The PTY is managed via the current process' TTY until it is closed.
|
||||
|
||||
Example:
|
||||
|
||||
import docker
|
||||
from dockerpty import PseudoTerminal
|
||||
|
||||
client = docker.Client()
|
||||
container = client.create_container(
|
||||
image='busybox:latest',
|
||||
stdin_open=True,
|
||||
tty=True,
|
||||
command='/bin/sh',
|
||||
)
|
||||
|
||||
# hijacks the current tty until the pty is closed
|
||||
PseudoTerminal(client, container).start()
|
||||
|
||||
Care is taken to ensure all file descriptors are restored on exit. For
|
||||
example, you can attach to a running container from within a Python REPL
|
||||
and when the container exits, the user will be returned to the Python REPL
|
||||
without adverse effects.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, client, container):
|
||||
"""
|
||||
Initialize the PTY using the docker.Client instance and container dict.
|
||||
"""
|
||||
|
||||
self.client = client
|
||||
self.container = container
|
||||
self.raw = None
|
||||
|
||||
|
||||
def start(self, **kwargs):
|
||||
"""
|
||||
Present the PTY of the container inside the current process.
|
||||
|
||||
This will take over the current process' TTY until the container's PTY
|
||||
is closed.
|
||||
"""
|
||||
|
||||
pty_stdin, pty_stdout, pty_stderr = self.sockets()
|
||||
|
||||
mappings = [
|
||||
(io.Stream(sys.stdin), pty_stdin),
|
||||
(pty_stdout, io.Stream(sys.stdout)),
|
||||
(pty_stderr, io.Stream(sys.stderr)),
|
||||
]
|
||||
|
||||
pumps = [io.Pump(a, b) for (a, b) in mappings if a and b]
|
||||
|
||||
if not self.container_info()['State']['Running']:
|
||||
self.client.start(self.container, **kwargs)
|
||||
|
||||
flags = [p.set_blocking(False) for p in pumps]
|
||||
|
||||
try:
|
||||
with WINCHHandler(self):
|
||||
self._hijack_tty(pumps)
|
||||
finally:
|
||||
if flags:
|
||||
for (pump, flag) in zip(pumps, flags):
|
||||
io.set_blocking(pump, flag)
|
||||
|
||||
|
||||
def israw(self):
|
||||
"""
|
||||
Returns True if the PTY should operate in raw mode.
|
||||
|
||||
If the container was not started with tty=True, this will return False.
|
||||
"""
|
||||
|
||||
if self.raw is None:
|
||||
info = self.container_info()
|
||||
self.raw = sys.stdout.isatty() and info['Config']['Tty']
|
||||
|
||||
return self.raw
|
||||
|
||||
|
||||
def sockets(self):
|
||||
"""
|
||||
Returns a tuple of sockets connected to the pty (stdin,stdout,stderr).
|
||||
|
||||
If any of the sockets are not attached in the container, `None` is
|
||||
returned in the tuple.
|
||||
"""
|
||||
|
||||
info = self.container_info()
|
||||
|
||||
def attach_socket(key):
|
||||
if info['Config']['Attach{0}'.format(key.capitalize())]:
|
||||
socket = self.client.attach_socket(
|
||||
self.container,
|
||||
{key: 1, 'stream': 1, 'logs': 1},
|
||||
)
|
||||
stream = io.Stream(socket)
|
||||
|
||||
if info['Config']['Tty']:
|
||||
return stream
|
||||
else:
|
||||
return io.Demuxer(stream)
|
||||
else:
|
||||
return None
|
||||
|
||||
return map(attach_socket, ('stdin', 'stdout', 'stderr'))
|
||||
|
||||
|
||||
def resize(self, size=None):
|
||||
"""
|
||||
Resize the container's PTY.
|
||||
|
||||
If `size` is not None, it must be a tuple of (height,width), otherwise
|
||||
it will be determined by the size of the current TTY.
|
||||
"""
|
||||
|
||||
if not self.israw():
|
||||
return
|
||||
|
||||
size = size or tty.size(sys.stdout)
|
||||
|
||||
if size is not None:
|
||||
rows, cols = size
|
||||
try:
|
||||
self.client.resize(self.container, height=rows, width=cols)
|
||||
except IOError: # Container already exited
|
||||
pass
|
||||
|
||||
|
||||
def container_info(self):
|
||||
"""
|
||||
Thin wrapper around client.inspect_container().
|
||||
"""
|
||||
|
||||
return self.client.inspect_container(self.container)
|
||||
|
||||
|
||||
def _hijack_tty(self, pumps):
|
||||
with tty.Terminal(sys.stdin, raw=self.israw()):
|
||||
self.resize()
|
||||
while True:
|
||||
_ready = io.select(pumps, timeout=60)
|
||||
try:
|
||||
if all([p.flush() is None for p in pumps]):
|
||||
break
|
||||
except SSLError as e:
|
||||
if 'The operation did not complete' not in e.strerror:
|
||||
raise e
|
||||
@@ -1,130 +0,0 @@
|
||||
# dockerpty: tty.py
|
||||
#
|
||||
# Copyright 2014 Chris Corbyn <chris@w3style.co.uk>
|
||||
#
|
||||
# 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 __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import termios
|
||||
import tty
|
||||
import fcntl
|
||||
import struct
|
||||
|
||||
|
||||
def size(fd):
|
||||
"""
|
||||
Return a tuple (rows,cols) representing the size of the TTY `fd`.
|
||||
|
||||
The provided file descriptor should be the stdout stream of the TTY.
|
||||
|
||||
If the TTY size cannot be determined, returns None.
|
||||
"""
|
||||
|
||||
if not os.isatty(fd.fileno()):
|
||||
return None
|
||||
|
||||
try:
|
||||
dims = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, 'hhhh'))
|
||||
except:
|
||||
try:
|
||||
dims = (os.environ['LINES'], os.environ['COLUMNS'])
|
||||
except:
|
||||
return None
|
||||
|
||||
return dims
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
"""
|
||||
Terminal provides wrapper functionality to temporarily make the tty raw.
|
||||
|
||||
This is useful when streaming data from a pseudo-terminal into the tty.
|
||||
|
||||
Example:
|
||||
|
||||
with Terminal(sys.stdin, raw=True):
|
||||
do_things_in_raw_mode()
|
||||
"""
|
||||
|
||||
def __init__(self, fd, raw=True):
|
||||
"""
|
||||
Initialize a terminal for the tty with stdin attached to `fd`.
|
||||
|
||||
Initializing the Terminal has no immediate side effects. The `start()`
|
||||
method must be invoked, or `with raw_terminal:` used before the
|
||||
terminal is affected.
|
||||
"""
|
||||
|
||||
self.fd = fd
|
||||
self.raw = raw
|
||||
self.original_attributes = None
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Invoked when a `with` block is first entered.
|
||||
"""
|
||||
|
||||
self.start()
|
||||
return self
|
||||
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""
|
||||
Invoked when a `with` block is finished.
|
||||
"""
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
def israw(self):
|
||||
"""
|
||||
Returns True if the TTY should operate in raw mode.
|
||||
"""
|
||||
|
||||
return self.raw
|
||||
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Saves the current terminal attributes and makes the tty raw.
|
||||
|
||||
This method returns None immediately.
|
||||
"""
|
||||
|
||||
if os.isatty(self.fd.fileno()) and self.israw():
|
||||
self.original_attributes = termios.tcgetattr(self.fd)
|
||||
tty.setraw(self.fd)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Restores the terminal attributes back to before setting raw mode.
|
||||
|
||||
If the raw terminal was not started, does nothing.
|
||||
"""
|
||||
|
||||
if self.original_attributes is not None:
|
||||
termios.tcsetattr(
|
||||
self.fd,
|
||||
termios.TCSADRAIN,
|
||||
self.original_attributes,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "{cls}({fd}, raw={raw})".format(
|
||||
cls=type(self).__name__,
|
||||
fd=self.fd,
|
||||
raw=self.raw)
|
||||
@@ -19,7 +19,9 @@ def stream_output(output, stream):
|
||||
all_events.append(event)
|
||||
|
||||
if 'progress' in event or 'progressDetail' in event:
|
||||
image_id = event['id']
|
||||
image_id = event.get('id')
|
||||
if not image_id:
|
||||
continue
|
||||
|
||||
if image_id in lines:
|
||||
diff = len(lines) - lines[image_id]
|
||||
|
||||
@@ -167,15 +167,14 @@ class Project(object):
|
||||
else:
|
||||
log.info('%s uses an image, skipping' % service.name)
|
||||
|
||||
def up(self, service_names=None, start_links=True, recreate=True):
|
||||
def up(self, service_names=None, start_links=True, recreate=True, insecure_registry=False):
|
||||
running_containers = []
|
||||
|
||||
for service in self.get_services(service_names, include_links=start_links):
|
||||
if recreate:
|
||||
for (_, container) in service.recreate_containers():
|
||||
for (_, container) in service.recreate_containers(insecure_registry=insecure_registry):
|
||||
running_containers.append(container)
|
||||
else:
|
||||
for container in service.start_or_create_containers():
|
||||
for container in service.start_or_create_containers(insecure_registry=insecure_registry):
|
||||
running_containers.append(container)
|
||||
|
||||
return running_containers
|
||||
|
||||
@@ -168,7 +168,7 @@ class Service(object):
|
||||
log.info("Removing %s..." % c.name)
|
||||
c.remove(**options)
|
||||
|
||||
def create_container(self, one_off=False, **override_options):
|
||||
def create_container(self, one_off=False, insecure_registry=False, **override_options):
|
||||
"""
|
||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||
it.
|
||||
@@ -179,21 +179,24 @@ class Service(object):
|
||||
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'])
|
||||
output = self.client.pull(container_options['image'], stream=True)
|
||||
output = self.client.pull(
|
||||
container_options['image'],
|
||||
stream=True,
|
||||
insecure_registry=insecure_registry
|
||||
)
|
||||
stream_output(output, sys.stdout)
|
||||
return Container.create(self.client, **container_options)
|
||||
raise
|
||||
|
||||
def recreate_containers(self, **override_options):
|
||||
def recreate_containers(self, insecure_registry=False, **override_options):
|
||||
"""
|
||||
If a container for this service doesn't exist, create and start one. If there are
|
||||
any, stop them, create+start new ones, and remove the old containers.
|
||||
"""
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
container = self.create_container(**override_options)
|
||||
container = self.create_container(insecure_registry=insecure_registry, **override_options)
|
||||
self.start_container(container)
|
||||
return [(None, container)]
|
||||
else:
|
||||
@@ -201,7 +204,7 @@ class Service(object):
|
||||
|
||||
for c in containers:
|
||||
log.info("Recreating %s..." % c.name)
|
||||
tuples.append(self.recreate_container(c, **override_options))
|
||||
tuples.append(self.recreate_container(c, insecure_registry=insecure_registry, **override_options))
|
||||
|
||||
return tuples
|
||||
|
||||
@@ -270,12 +273,12 @@ class Service(object):
|
||||
)
|
||||
return container
|
||||
|
||||
def start_or_create_containers(self):
|
||||
def start_or_create_containers(self, insecure_registry=False):
|
||||
containers = self.containers(stopped=True)
|
||||
|
||||
if not containers:
|
||||
log.info("Creating %s..." % self._next_container_name(containers))
|
||||
new_container = self.create_container()
|
||||
new_container = self.create_container(insecure_registry=insecure_registry)
|
||||
return [self.start_container(new_container)]
|
||||
else:
|
||||
return [self.start_container_if_stopped(c) for c in containers]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
PyYAML==3.10
|
||||
docker-py==0.5.3
|
||||
dockerpty==0.3.2
|
||||
docopt==0.6.1
|
||||
requests==2.2.1
|
||||
six==1.7.3
|
||||
|
||||
11
script/test
11
script/test
@@ -1,12 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -ex
|
||||
|
||||
target="tests"
|
||||
|
||||
if [[ -n "$@" ]]; then
|
||||
target="$@"
|
||||
fi
|
||||
|
||||
docker build -t fig .
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 --exclude=packages fig
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $target
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 fig
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $@
|
||||
|
||||
5
setup.py
5
setup.py
@@ -30,7 +30,8 @@ install_requires = [
|
||||
'requests >= 2.2.1, < 3',
|
||||
'texttable >= 0.8.1, < 0.9',
|
||||
'websocket-client >= 0.11.0, < 0.12',
|
||||
'docker-py >= 0.5, < 0.6',
|
||||
'docker-py >= 0.5.3, < 0.6',
|
||||
'dockerpty >= 0.3.2, < 0.4',
|
||||
'six >= 1.3.0, < 2',
|
||||
]
|
||||
|
||||
@@ -49,7 +50,7 @@ if sys.version_info < (2, 7):
|
||||
setup(
|
||||
name='fig',
|
||||
version=find_version("fig", "__init__.py"),
|
||||
description='Punctual, lightweight development environments using Docker',
|
||||
description='Fast, isolated development environments using Docker',
|
||||
url='http://www.fig.sh/',
|
||||
author='Docker, Inc.',
|
||||
license='Apache License 2.0',
|
||||
|
||||
@@ -25,6 +25,16 @@ class CLITestCase(DockerClientTestCase):
|
||||
def project(self):
|
||||
return self.command.get_project(self.command.get_config_path())
|
||||
|
||||
def test_help(self):
|
||||
old_base_dir = self.command.base_dir
|
||||
self.command.base_dir = 'tests/fixtures/no-figfile'
|
||||
with self.assertRaises(SystemExit) as exc_context:
|
||||
self.command.dispatch(['help', 'up'], None)
|
||||
self.assertIn('Usage: up [options] [SERVICE...]', str(exc_context.exception))
|
||||
# self.project.kill() fails during teardown
|
||||
# unless there is a figfile.
|
||||
self.command.base_dir = old_base_dir
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_ps(self, mock_stdout):
|
||||
self.project.get_service('simple').create_container()
|
||||
@@ -129,13 +139,13 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
self.assertEqual(old_ids, new_ids)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_without_links(self, mock_stdout):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', 'console', '/bin/true'], None)
|
||||
self.assertEqual(len(self.project.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_links(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', 'web', '/bin/true'], None)
|
||||
@@ -144,14 +154,14 @@ class CLITestCase(DockerClientTestCase):
|
||||
self.assertEqual(len(db.containers()), 1)
|
||||
self.assertEqual(len(console.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_with_no_deps(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
|
||||
db = self.project.get_service('db')
|
||||
self.assertEqual(len(db.containers()), 0)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_does_not_recreate_linked_containers(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/links-figfile'
|
||||
self.command.dispatch(['up', '-d', 'db'], None)
|
||||
@@ -167,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
|
||||
|
||||
self.assertEqual(old_ids, new_ids)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_without_command(self, __):
|
||||
self.command.base_dir = 'tests/fixtures/commands-figfile'
|
||||
self.check_build('tests/fixtures/simple-dockerfile', tag='figtest_test')
|
||||
@@ -191,7 +201,7 @@ class CLITestCase(DockerClientTestCase):
|
||||
[u'/bin/true'],
|
||||
)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_entrypoint_overridden(self, _):
|
||||
self.command.base_dir = 'tests/fixtures/dockerfile_with_entrypoint'
|
||||
name = 'service'
|
||||
@@ -206,7 +216,7 @@ class CLITestCase(DockerClientTestCase):
|
||||
u'/bin/echo helloworld'
|
||||
)
|
||||
|
||||
@patch('fig.packages.dockerpty.start')
|
||||
@patch('dockerpty.start')
|
||||
def test_run_service_with_environement_overridden(self, _):
|
||||
name = 'service'
|
||||
self.command.base_dir = 'tests/fixtures/environment-figfile'
|
||||
|
||||
16
tests/unit/cli/docker_client_test.py
Normal file
16
tests/unit/cli/docker_client_test.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
import mock
|
||||
from tests import unittest
|
||||
|
||||
from fig.cli import docker_client
|
||||
|
||||
|
||||
class DockerClientTestCase(unittest.TestCase):
|
||||
|
||||
def test_docker_client_no_home(self):
|
||||
with mock.patch.dict(os.environ):
|
||||
del os.environ['HOME']
|
||||
docker_client.docker_client()
|
||||
@@ -5,7 +5,7 @@ from tests import unittest
|
||||
from fig.cli import verbose_proxy
|
||||
|
||||
|
||||
class VerboseProxy(unittest.TestCase):
|
||||
class VerboseProxyTestCase(unittest.TestCase):
|
||||
|
||||
def test_format_call(self):
|
||||
expected = "(u'arg1', True, key=u'value')"
|
||||
|
||||
20
tests/unit/progress_stream_test.py
Normal file
20
tests/unit/progress_stream_test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from tests import unittest
|
||||
|
||||
import mock
|
||||
from six import StringIO
|
||||
|
||||
from fig import progress_stream
|
||||
|
||||
|
||||
class ProgressStreamTestCase(unittest.TestCase):
|
||||
|
||||
def test_stream_output(self):
|
||||
output = [
|
||||
'{"status": "Downloading", "progressDetail": {"current": '
|
||||
'31019763, "start": 1413653874, "total": 62763875}, '
|
||||
'"progress": "..."}',
|
||||
]
|
||||
events = progress_stream.stream_output(output, StringIO())
|
||||
self.assertEqual(len(events), 1)
|
||||
@@ -6,6 +6,7 @@ from .. import unittest
|
||||
import mock
|
||||
|
||||
import docker
|
||||
from requests import Response
|
||||
|
||||
from fig import Service
|
||||
from fig.container import Container
|
||||
@@ -14,6 +15,7 @@ from fig.service import (
|
||||
split_port,
|
||||
parse_volume_spec,
|
||||
build_volume_binding,
|
||||
APIError,
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +176,21 @@ class ServiceTest(unittest.TestCase):
|
||||
self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True)
|
||||
mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
|
||||
|
||||
@mock.patch('fig.service.log', autospec=True)
|
||||
def test_create_container_from_insecure_registry(self, mock_log):
|
||||
service = Service('foo', client=self.mock_client, image='someimage:sometag')
|
||||
mock_response = mock.Mock(Response)
|
||||
mock_response.status_code = 404
|
||||
mock_response.reason = "Not Found"
|
||||
Container.create = mock.Mock()
|
||||
Container.create.side_effect = APIError('Mock error', mock_response, "No such image")
|
||||
try:
|
||||
service.create_container(insecure_registry=True)
|
||||
except APIError: # We expect the APIError because our service requires a non-existent image.
|
||||
pass
|
||||
self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True, stream=True)
|
||||
mock_log.info.assert_called_once_with('Pulling image someimage:sometag...')
|
||||
|
||||
|
||||
class ServiceVolumesTest(unittest.TestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user