Compare commits

..

20 Commits
0.1.1 ... 0.1.2

Author SHA1 Message Date
Ben Firshman
deb7f3c5b6 Bump to version 0.1.2 2014-01-22 13:37:37 +00:00
Ben Firshman
48eb5e5c82 Merge pull request #40 from orchardup/fix-35
Make sure attach() is called as soon as LogPrinter is initialized
2014-01-22 05:14:05 -08:00
Aanand Prasad
65071aafb0 Make sure attach() is called as soon as LogPrinter is initialized
Fixes #35.
2014-01-22 13:12:51 +00:00
Ben Firshman
4ee87a7029 Merge pull request #39 from orchardup/fix-cursor-lag
Fix lag when using cursor keys in an interactive 'fig run'
2014-01-20 16:12:29 -08:00
Aanand Prasad
977ec7c941 Remove unused import 2014-01-20 19:25:28 +00:00
Aanand Prasad
40d04a076c Fix lag when using cursor keys in an interactive 'fig run' 2014-01-20 19:23:50 +00:00
Ben Firshman
4646ac85b0 Add PyPi badge 2014-01-20 18:41:04 +00:00
Ben Firshman
8773bad99a Merge pull request #34 from orchardup/better-tty-handling-for-fig-run
Add option to disable pseudo-tty on fig run
2014-01-20 10:19:13 -08:00
Aanand Prasad
084db337a0 Update docker-py
Brought in changes from https://github.com/dotcloud/docker-py/pull/145
2014-01-20 18:09:25 +00:00
Aanand Prasad
f47f075f02 Merge pull request #36 from orchardup/use-container-create-for-recreating-containers
Use Container.create to recreate containers
2014-01-20 09:59:02 -08:00
Ben Firshman
7abc4fbf3a Improve ps CLI test 2014-01-20 16:50:41 +00:00
Ben Firshman
855a9c623c Remove containers after running CLI tests 2014-01-20 16:47:58 +00:00
Ben Firshman
7e2d86c510 Use Container.create to recreate containers
self.create_container might do unexpected things.
2014-01-20 16:10:54 +00:00
Aanand Prasad
405079f744 Use raw socket in 'fig run', simplify _attach_to_container 2014-01-20 15:52:07 +00:00
Ben Firshman
fc1bbb45b1 Add option to disable pseudo-tty on fig run
Also disable tty if stdin is not a tty.
2014-01-19 20:33:06 +00:00
Ben Firshman
24a6d1d836 Forcibly kill Docker when Travis ends
May fix tests timing out.
2014-01-19 20:11:53 +00:00
Ben Firshman
f3d273864d Add comments to script/travis 2014-01-19 20:11:53 +00:00
Ben Firshman
ce8ef7afe7 Merge pull request #33 from cameronmaske/recreate_containers
Patch for #32
2014-01-19 12:11:05 -08:00
Cameron Maske
62bba1684b Updated recreate_containers to attempt to base intermediate container's the previous container's image.
Added in additional functionality to reset any entrypoints for the intermediate container and pull/retry handling if the image does not exist.
Updated test coverage to check if an container is recreated with an entrypoint it is handled correctly.
2014-01-19 18:40:21 +00:00
Ben Firshman
07f3c78369 Update docker-py
From 4bc5d27e51
2014-01-19 16:50:08 +00:00
13 changed files with 122 additions and 99 deletions

View File

@@ -1,6 +1,13 @@
Change log
==========
0.1.2 (2014-01-22)
------------------
- Add `-T` option to `fig run` to disable pseudo-TTY. (#34)
- Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske!
- Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40)
0.1.1 (2014-01-17)
------------------

View File

@@ -2,6 +2,7 @@ Fig
===
[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig)
[![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig)
Punctual, lightweight development environments using Docker.

View File

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

View File

@@ -31,8 +31,9 @@ class LogPrinter(object):
def _make_log_generator(self, container, color_fn):
prefix = color_fn(container.name + " | ")
for line in split_buffer(self._attach(container), '\n'):
yield prefix + line
# Attach to container before log printer starts running
line_generator = split_buffer(self._attach(container), '\n')
return (prefix + line for line in line_generator)
def _attach(self, container):
params = {

View File

@@ -4,7 +4,6 @@ import logging
import sys
import re
import signal
import sys
from inspect import getdoc
@@ -200,12 +199,20 @@ class TopLevelCommand(Command):
Usage: run [options] SERVICE COMMAND [ARGS...]
Options:
-d Detached mode: Run container in the background, print new container name
-d Detached mode: Run container in the background, print new
container name
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
"""
service = self.project.get_service(options['SERVICE'])
tty = True
if options['-d'] or options['-T'] or not sys.stdin.isatty():
tty = False
container_options = {
'command': [options['COMMAND']] + options['ARGS'],
'tty': not options['-d'],
'tty': tty,
'stdin_open': not options['-d'],
}
container = service.create_container(one_off=True, **container_options)
@@ -213,12 +220,7 @@ class TopLevelCommand(Command):
service.start_container(container, ports=None)
print(container.name)
else:
with self._attach_to_container(
container.id,
interactive=True,
logs=True,
raw=True
) as c:
with self._attach_to_container(container.id, raw=tty) as c:
service.start_container(container, ports=None)
c.run()
@@ -310,35 +312,15 @@ class TopLevelCommand(Command):
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(
container_id,
params={
'stdin': 1 if interactive else 0,
'stdout': 1,
'stderr': 0,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
)
stderr = self.client.attach_socket(
container_id,
params={
'stdin': 0,
'stdout': 0,
'stderr': 1,
'logs': 1 if logs else 0,
'stream': 1 if stream else 0
},
ws=True,
)
def _attach_to_container(self, container_id, raw=False):
socket_in = self.client.attach_socket(container_id, params={'stdin': 1, 'stream': 1})
socket_out = self.client.attach_socket(container_id, params={'stdout': 1, 'logs': 1, 'stream': 1})
socket_err = self.client.attach_socket(container_id, params={'stderr': 1, 'logs': 1, 'stream': 1})
return SocketClient(
socket_in=stdio,
socket_out=stdio,
socket_err=stderr,
socket_in=socket_in,
socket_out=socket_out,
socket_err=socket_err,
raw=raw,
)

View File

@@ -1,7 +1,6 @@
from __future__ import print_function
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
from select import select
import sys
import tty
import fcntl
@@ -57,15 +56,15 @@ class SocketClient:
def run(self):
if self.socket_in is not None:
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
self.start_background_thread(target=self.send, args=(self.socket_in, sys.stdin))
recv_threads = []
if self.socket_out is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout)))
recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_out, sys.stdout)))
if self.socket_err is not None:
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr)))
recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_err, sys.stderr)))
for t in recv_threads:
t.join()
@@ -76,10 +75,10 @@ class SocketClient:
thread.start()
return thread
def recv_ws(self, socket, stream):
def recv(self, socket, stream):
try:
while True:
chunk = socket.recv()
chunk = socket.recv(4096)
if chunk:
stream.write(chunk)
@@ -89,24 +88,21 @@ class SocketClient:
except Exception as e:
log.debug(e)
def send_ws(self, socket, stream):
def send(self, socket, stream):
while True:
r, w, e = select([stream.fileno()], [], [])
chunk = stream.read(1)
if r:
chunk = stream.read(1)
if chunk == '':
socket.send_close()
break
else:
try:
socket.send(chunk)
except Exception as e:
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
break
else:
raise e
if chunk == '':
socket.close()
break
else:
try:
socket.send(chunk)
except Exception as e:
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
break
else:
raise e
def destroy(self):
if self.settings is not None:

View File

@@ -3,7 +3,7 @@ from __future__ import absolute_import
class Container(object):
"""
Represents a Docker container, constructed from the output of
Represents a Docker container, constructed from the output of
GET /containers/:id:/json.
"""
def __init__(self, client, dictionary, has_been_inspected=False):
@@ -38,6 +38,10 @@ class Container(object):
def id(self):
return self.dictionary['ID']
@property
def image(self):
return self.dictionary['Image']
@property
def short_id(self):
return self.id[:10]

View File

@@ -122,7 +122,8 @@ class Client(requests.Session):
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False):
network_disabled=False, entrypoint=None,
cpu_shares=None, working_dir=None):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
@@ -134,15 +135,12 @@ class Client(requests.Session):
exposed_ports = {}
for port_definition in ports:
port = port_definition
proto = None
proto = 'tcp'
if isinstance(port_definition, tuple):
if len(port_definition) == 2:
proto = port_definition[1]
port = port_definition[0]
exposed_ports['{0}{1}'.format(
port,
'/' + proto if proto else ''
)] = {}
exposed_ports['{0}/{1}'.format(port, proto)] = {}
ports = exposed_ports
if volumes and isinstance(volumes, list):
@@ -154,6 +152,7 @@ class Client(requests.Session):
attach_stdin = False
attach_stdout = False
attach_stderr = False
stdin_once = False
if not detach:
attach_stdout = True
@@ -161,6 +160,7 @@ class Client(requests.Session):
if stdin_open:
attach_stdin = True
stdin_once = True
return {
'Hostname': hostname,
@@ -168,6 +168,7 @@ class Client(requests.Session):
'User': user,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,
'Memory': mem_limit,
'AttachStdin': attach_stdin,
'AttachStdout': attach_stdout,
@@ -178,7 +179,10 @@ class Client(requests.Session):
'Image': image,
'Volumes': volumes,
'VolumesFrom': volumes_from,
'NetworkDisabled': network_disabled
'NetworkDisabled': network_disabled,
'Entrypoint': entrypoint,
'CpuShares': cpu_shares,
'WorkingDir': working_dir
}
def _post_json(self, url, data, **kwargs):
@@ -409,11 +413,13 @@ class Client(requests.Session):
detach=False, stdin_open=False, tty=False,
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, name=None):
network_disabled=False, name=None, entrypoint=None,
cpu_shares=None, working_dir=None):
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from, network_disabled
ports, environment, dns, volumes, volumes_from, network_disabled,
entrypoint, cpu_shares, working_dir
)
return self.create_container_from_config(config, name)
@@ -475,27 +481,34 @@ class Client(requests.Session):
return [x['Id'] for x in res]
return res
def import_image(self, src, data=None, repository=None, tag=None):
def import_image(self, src=None, repository=None, tag=None, image=None):
u = self._url("/images/create")
params = {
'repo': repository,
'tag': tag
}
try:
# XXX: this is ways not optimal but the only way
# for now to import tarballs through the API
fic = open(src)
data = fic.read()
fic.close()
src = "-"
except IOError:
# file does not exists or not a file (URL)
data = None
if isinstance(src, six.string_types):
params['fromSrc'] = src
return self._result(self._post(u, data=data, params=params))
return self._result(self._post(u, data=src, params=params))
if src:
try:
# XXX: this is ways not optimal but the only way
# for now to import tarballs through the API
fic = open(src)
data = fic.read()
fic.close()
src = "-"
except IOError:
# file does not exists or not a file (URL)
data = None
if isinstance(src, six.string_types):
params['fromSrc'] = src
return self._result(self._post(u, data=data, params=params))
return self._result(self._post(u, data=src, params=params))
if image:
params['fromImage'] = image
return self._result(self._post(u, data=None, params=params))
raise Exception("Must specify a src or image")
def info(self):
return self._result(self._get(self._url("/info")),
@@ -577,13 +590,13 @@ class Client(requests.Session):
self._raise_for_status(res)
json_ = res.json()
s_port = str(private_port)
f_port = None
if s_port in json_['NetworkSettings']['PortMapping']['Udp']:
f_port = json_['NetworkSettings']['PortMapping']['Udp'][s_port]
elif s_port in json_['NetworkSettings']['PortMapping']['Tcp']:
f_port = json_['NetworkSettings']['PortMapping']['Tcp'][s_port]
h_ports = None
return f_port
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp')
if h_ports is None:
h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp')
return h_ports
def pull(self, repository, tag=None, stream=False):
registry, repo_name = auth.resolve_repository_name(repository)

View File

@@ -143,9 +143,10 @@ class Service(object):
intermediate_container = Container.create(
self.client,
image='ubuntu',
image=container.image,
command='echo',
volumes_from=container.id,
entrypoint=None
)
intermediate_container.start()
intermediate_container.wait()
@@ -212,7 +213,7 @@ class Service(object):
return links
def _get_container_options(self, override_options, one_off=False):
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from']
keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint']
container_options = dict((k, self.options[k]) for k in keys if k in self.options)
container_options.update(override_options)

View File

@@ -5,3 +5,4 @@ PyYAML==3.10
texttable==0.8.1
# docker requires six==1.3.0
six==1.3.0
mock==1.0.1

View File

@@ -3,14 +3,17 @@
# Exit on first error
set -ex
# Put Python eggs in a writeable directory
export PYTHON_EGG_CACHE="/tmp/.python-eggs"
# Activate correct virtualenv
TRAVIS_PYTHON_VERSION=$1
source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate
env
# Kill background processes on exit
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT
# Start docker daemon
docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null &

View File

@@ -1,6 +1,8 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from . import unittest
from mock import patch
from six import StringIO
from fig.cli.main import TopLevelCommand
class CLITestCase(unittest.TestCase):
@@ -8,11 +10,18 @@ class CLITestCase(unittest.TestCase):
self.command = TopLevelCommand()
self.command.base_dir = 'tests/fixtures/simple-figfile'
def tearDown(self):
self.command.project.kill()
self.command.project.remove_stopped()
def test_help(self):
self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None))
def test_ps(self):
@patch('sys.stdout', new_callable=StringIO)
def test_ps(self, mock_stdout):
self.command.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
def test_scale(self):
project = self.command.project

View File

@@ -110,8 +110,9 @@ class ServiceTest(DockerClientTestCase):
self.assertIn('/var/db', container.inspect()['Volumes'])
def test_recreate_containers(self):
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'])
service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps'])
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(old_container.dictionary['Config']['Env'], ['FOO=1'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
@@ -120,11 +121,15 @@ class ServiceTest(DockerClientTestCase):
num_containers_before = len(self.client.containers(all=True))
service.options['environment']['FOO'] = '2'
(old, new) = service.recreate_containers()
self.assertEqual(len(old), 1)
(intermediate, new) = service.recreate_containers()
self.assertEqual(len(intermediate), 1)
self.assertEqual(len(new), 1)
new_container = new[0]
intermediate_container = intermediate[0]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None)
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2'])
self.assertEqual(new_container.name, 'figtest_db_1')
service.start_container(new_container)