Compare commits

...

41 Commits
1.0.0 ... 1.0.1

Author SHA1 Message Date
Aanand Prasad
3f4b16181d Merge pull request #618 from bfirsh/make-dnephin-a-maintainer
Make @dnephin a maintainer
2014-11-05 10:58:50 +00:00
Aanand Prasad
5dde2a2498 Merge pull request #612 from bfirsh/ship-1.0.1
Ship 1.0.1
2014-11-05 10:57:53 +00:00
Ben Firshman
06a1b32c12 Make @dnephin a maintainer
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-11-05 09:47:48 +00:00
Ben Firshman
46433c70b6 Ship 1.0.1
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-11-04 14:12:48 +00:00
Aanand Prasad
9260603149 Merge pull request #588 from bfirsh/use-upstream-dockerpty
Use upstream dockerpty 0.3.2
2014-11-04 12:59:01 +00:00
Ben Firshman
8773f51583 Don't select stdin when interactive=False
Patch from https://github.com/d11wtq/dockerpty/pull/24

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-11-04 10:25:27 +00:00
Ben Firshman
38a3ee8d63 Use upstream dockerpty 0.3.2
This reverts commit 60411e9f05.

Closes #556

Signed-off-by: Ben Firshman <ben@firshman.co.uk>

Conflicts:
	requirements.txt
	setup.py
2014-11-04 10:23:42 +00:00
Aanand Prasad
cb275b8633 Merge pull request #591 from npeters/fix-insecure-registry
fix insecure parameter
2014-11-03 14:17:43 +00:00
Aanand Prasad
604b370bb4 Merge pull request #606 from kojiromike/602-no-help-without-figfile
Fixes #602 Allowing `help $cmd` with no figfile
2014-11-03 11:27:58 +00:00
Michael A. Smith
782a46fd60 Fixes #602 Allowing help $cmd with no figfile
Signed-off-by: Michael A. Smith <michael@smith-li.com>
2014-11-02 06:22:16 -05:00
Aanand Prasad
1bd3d0dd77 Merge pull request #584 from kevinsimper/patch-1
Update wordpress url on wordpress example
2014-10-31 12:23:30 +00:00
Nicolas Peters
b64ea85916 fix insecure parameter
Signed-off-by: Nicolas Peters <peters.nico@gmail.com>
2014-10-28 00:05:17 +01:00
Kevin Simper
b47ab2b0f6 Update wordpress url on wordpress example
The old url gave a 302 and wordpress 4.0 has also been released.
2014-10-24 23:50:06 +02:00
Aanand Prasad
11280e4f30 Merge pull request #581 from bfirsh/add-dockerignore
Add .dockerignore
2014-10-24 11:59:01 +01:00
Aanand Prasad
a83876da09 Merge pull request #580 from bfirsh/improve-contributing-instructions
Improve contributing instructions
2014-10-24 11:43:14 +01:00
Ben Firshman
e66c0452d5 Add .dockerignore
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-23 19:00:44 +01:00
Ben Firshman
899670fc6c Move building binaries instructions
It's less important than signing your work.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-23 18:31:28 +01:00
Ben Firshman
ea45715a50 Tidied up development environment instructions
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-23 18:30:48 +01:00
Ben Firshman
28f9c8d047 Add TL;DR section to CONTRIBUTING.md
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-23 18:28:17 +01:00
Aanand Prasad
292fe6640e Merge pull request #578 from bfirsh/update-url-in-twitter-button
Update Twitter button URL
2014-10-23 16:17:38 +01:00
Ben Firshman
b759b9854a Update Twitter button URL
I originally left it as orchardup.github.io so the number wouldn't roll
back to 0, but people have been tweeting about fig.sh now.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-23 16:05:11 +01:00
Aanand Prasad
40943d5c81 Merge pull request #533 from bfirsh/mention-configuration-files-on-home-page
Mention names of config files on home page
2014-10-23 11:23:33 +01:00
Aanand Prasad
93a0195dc8 Merge branch 'andreagrandi-patch-1'
Closes #570.
2014-10-23 11:14:53 +01:00
Andrea Grandi
7f0745d146 Add command line options to the documentation
Signed-off-by: Andrea Grandi <a.grandi@gmail.com>
2014-10-23 11:13:40 +01:00
Ben Firshman
9813a8d5be Merge pull request #562 from tjrivera/insecure-registry
Allow dependent image pull from insecure registry
2014-10-22 18:45:03 +01:00
Tyler Rivera
491181ec31 Allow dependent image pull from insecure registry
PR #490 Provides the ability to pull from an insecure registry by passing --allow-insecure-ssl. This commit extends the work done in #490 and adds the ability to pass --allow-insecure-ssl to the up and run commands which will attempt to pull dependent images if they do not exist.

Signed-off-by: Tyler Rivera <riverat2@email.chop.edu>
2014-10-22 10:57:43 -04:00
Aanand Prasad
e1ad5b1b99 Merge pull request #575 from bfirsh/fix-google-analytics-code
Fix Google Analytics code
2014-10-22 12:48:45 +01:00
Aanand Prasad
67d4b7c587 Merge pull request #574 from bfirsh/update-tagline
Update tagline to match website
2014-10-22 12:46:16 +01:00
Ben Firshman
98f663bab2 Fix Google Analytics code
It was not reporting because our URL changed.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-22 12:46:05 +01:00
Ben Firshman
75b3dcf5fd Update tagline to match website
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-22 12:41:01 +01:00
Ben Firshman
deeb6c2236 Merge pull request #567 from aanand/fix-wordpress-guide
Fix language in Wordpress guide
2014-10-22 11:31:41 +01:00
Aanand Prasad
e1e2b75691 Fix language in Wordpress guide
Was still talking about 'environment variables'.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-21 11:44:42 +01:00
Aanand Prasad
392c118bbc Merge pull request #554 from dnephin/more_defensive_stream
Fix a couple bugs with 1.0 release and Docker 1.3
2014-10-20 15:10:50 +01:00
Ben Firshman
dbd1e56dd3 Merge pull request #548 from aanand/fix-docker-py-requirement
Fix docker-py requirement in setup.py
2014-10-20 10:33:44 +01:00
Daniel Nephin
7544580b4b Resolves #553, Resolves #546 - bug fixes with unit tests
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2014-10-18 13:54:10 -04:00
Aanand Prasad
2efb4f5be0 Merge pull request #550 from bfirsh/fix-date-on-change-notes
Fix date on 1.0.0 change notes
2014-10-17 15:47:50 +01:00
Ben Firshman
5e8dc6c972 Merge pull request #549 from aanand/credit-1.0-contributors
Credit 1.0 contributors
2014-10-17 15:43:34 +01:00
Ben Firshman
23c8ec8930 Fix date on 1.0.0 change notes
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-17 15:43:14 +01:00
Aanand Prasad
bd8affb7aa Credit 1.0 contributors
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-17 14:54:46 +01:00
Aanand Prasad
7a943739cb Fix docker-py requirement in setup.py
I updated requirements.txt but forgot about setup.py, so the version on
pypi is broken for people who already have docker-py 0.5.0 installed.

Closes #547.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-10-17 11:17:58 +01:00
Ben Firshman
d3f88cace5 Mention names of config files on home page
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-10-10 11:06:49 +01:00
30 changed files with 192 additions and 764 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
.git

View File

@@ -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)
------------------

View File

@@ -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

View File

@@ -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)

View File

@@ -52,7 +52,7 @@
<div class="badges">
<iframe src="http://ghbtns.com/github-btn.html?user=docker&amp;repo=fig&amp;type=watch&amp;count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://orchardup.github.io/fig/">Tweet</a>
<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>

View File

@@ -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

View File

@@ -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:

View File

@@ -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):

View File

@@ -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

View File

@@ -1,4 +1,4 @@
from __future__ import unicode_literals
from .service import Service # noqa:flake8
__version__ = '1.0.0'
__version__ = '1.0.1'

View File

@@ -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),

View File

@@ -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

View File

@@ -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()]

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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 $@

View File

@@ -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',

View File

@@ -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'

View 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()

View File

@@ -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')"

View 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)

View File

@@ -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):

View File

@@ -13,3 +13,4 @@ commands =
[flake8]
# ignore line-length for now
ignore = E501,E203
exclude = fig/packages