diff --git a/CHANGES.md b/CHANGES.md index 8a256296000..57e958aac96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,13 @@ Change log ========== +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) ------------------ diff --git a/MAINTAINERS b/MAINTAINERS index 6562a6b953d..8c98b04c32a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,4 +1,5 @@ Aanand Prasad (@aanand) Ben Firshman (@bfirsh) Chris Corbyn (@d11wtq) +Daniel Nephin (@dnephin) Nathan LeClaire (@nathanleclaire) diff --git a/docs/cli.md b/docs/cli.md index 4462575df02..822f1b78030 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -42,7 +42,9 @@ Get help on a command. ### kill -Force stop service containers. +Force stop running containers by sending a `SIGKILL` signal. Optionally the signal can be passed, for example: + + $ fig kill -s SIGINT ### logs diff --git a/docs/install.md b/docs/install.md index 6722b4f0154..14fd64c8c58 100644 --- a/docs/install.md +++ b/docs/install.md @@ -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): diff --git a/fig/__init__.py b/fig/__init__.py index 9f854545fde..a7b29e0c909 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service # noqa:flake8 -__version__ = '1.0.0' +__version__ = '1.0.1' diff --git a/fig/cli/main.py b/fig/cli/main.py index 2ce8cfc3268..e98fea8666e 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -7,7 +7,7 @@ from operator import attrgetter from inspect import getdoc -from fig.packages import dockerpty +import dockerpty from .. import __version__ from ..project import NoSuchService, ConfigurationError @@ -133,9 +133,15 @@ def kill(self, project, options): """ Force stop service containers. - Usage: kill [SERVICE...] + Usage: kill [options] [SERVICE...] + + Options: + -s SIGNAL SIGNAL to send to the container. + Default signal is SIGKILL. """ - project.kill(service_names=options['SERVICE']) + signal = options.get('-s', 'SIGKILL') + + project.kill(service_names=options['SERVICE'], signal=signal) def logs(self, project, options): """ @@ -323,7 +329,7 @@ def run(self, project, options): 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) diff --git a/fig/container.py b/fig/container.py index 7e06bde3550..0ab75512062 100644 --- a/fig/container.py +++ b/fig/container.py @@ -124,8 +124,8 @@ def start(self, **options): def stop(self, **options): return self.client.stop(self.id, **options) - def kill(self): - return self.client.kill(self.id) + def kill(self, **options): + return self.client.kill(self.id, **options) def restart(self): return self.client.restart(self.id) diff --git a/fig/packages/__init__.py b/fig/packages/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/fig/packages/dockerpty/__init__.py b/fig/packages/dockerpty/__init__.py deleted file mode 100644 index a5d707a4740..00000000000 --- a/fig/packages/dockerpty/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# dockerpty. -# -# Copyright 2014 Chris Corbyn -# -# 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() diff --git a/fig/packages/dockerpty/io.py b/fig/packages/dockerpty/io.py deleted file mode 100644 index c31c54010f0..00000000000 --- a/fig/packages/dockerpty/io.py +++ /dev/null @@ -1,294 +0,0 @@ -# dockerpty: io.py -# -# Copyright 2014 Chris Corbyn -# -# 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) diff --git a/fig/packages/dockerpty/pty.py b/fig/packages/dockerpty/pty.py deleted file mode 100644 index 4e11ca0aa9e..00000000000 --- a/fig/packages/dockerpty/pty.py +++ /dev/null @@ -1,235 +0,0 @@ -# dockerpty: pty.py -# -# Copyright 2014 Chris Corbyn -# -# 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 diff --git a/fig/packages/dockerpty/tty.py b/fig/packages/dockerpty/tty.py deleted file mode 100644 index bd2ccb5add9..00000000000 --- a/fig/packages/dockerpty/tty.py +++ /dev/null @@ -1,130 +0,0 @@ -# dockerpty: tty.py -# -# Copyright 2014 Chris Corbyn -# -# 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) diff --git a/fig/service.py b/fig/service.py index e6dcf01281b..bbbef7bc43e 100644 --- a/fig/service.py +++ b/fig/service.py @@ -251,7 +251,7 @@ def start_container_if_stopped(self, container, **options): def start_container(self, container=None, intermediate_container=None, **override_options): container = container or self.create_container(**override_options) options = dict(self.options, **override_options) - ports = dict(split_port(port) for port in options.get('ports') or []) + port_bindings = build_port_bindings(options.get('ports') or []) volume_bindings = dict( build_volume_binding(parse_volume_spec(volume)) @@ -264,7 +264,7 @@ def start_container(self, container=None, intermediate_container=None, **overrid container.start( links=self._get_links(link_to_self=options.get('one_off', False)), - port_bindings=ports, + port_bindings=port_bindings, binds=volume_bindings, volumes_from=self._get_volumes_from(intermediate_container), privileged=privileged, @@ -492,6 +492,17 @@ def build_volume_binding(volume_spec): return os.path.abspath(os.path.expandvars(external)), internal +def build_port_bindings(ports): + port_bindings = {} + for port in ports: + internal_port, external = split_port(port) + if internal_port in port_bindings: + port_bindings[internal_port].append(external) + else: + port_bindings[internal_port] = [external] + return port_bindings + + def split_port(port): parts = str(port).split(':') if not 1 <= len(parts) <= 3: diff --git a/requirements.txt b/requirements.txt index 1ed9a95dcf3..59aa90f02f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/script/test b/script/test index 2ed3f6b4fbb..79cc7e6b2a3 100755 --- a/script/test +++ b/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 $@ diff --git a/setup.py b/setup.py index a4aa23a62d0..fe839f5ad5b 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ def find_version(*file_paths): 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.11.0, < 0.12', 'docker-py >= 0.5.3, < 0.6', + 'dockerpty >= 0.3.2, < 0.4', 'six >= 1.3.0, < 2', ] diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index ceb93f62bcd..f03d72d2d88 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -84,6 +84,7 @@ def test_build_no_cache(self, mock_stdout): self.command.dispatch(['build', '--no-cache', 'simple'], None) output = mock_stdout.getvalue() self.assertNotIn(cache_indicator, output) + def test_up(self): self.command.dispatch(['up', '-d'], None) service = self.project.get_service('simple') @@ -139,13 +140,13 @@ def test_up_with_keep_old(self): 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) @@ -154,14 +155,14 @@ def test_run_service_with_links(self, __): 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) @@ -177,7 +178,7 @@ def test_run_does_not_recreate_linked_containers(self, __): 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') @@ -201,7 +202,7 @@ def test_run_without_command(self, __): [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' @@ -216,7 +217,7 @@ def test_run_service_with_entrypoint_overridden(self, _): 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' @@ -244,6 +245,40 @@ def test_rm(self): self.command.dispatch(['rm', '--force'], None) self.assertEqual(len(service.containers(stopped=True)), 0) + def test_kill(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.assertEqual(len(service.containers()), 1) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill'], None) + + self.assertEqual(len(service.containers(stopped=True)), 1) + self.assertFalse(service.containers(stopped=True)[0].is_running) + + def test_kill_signal_sigint(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.assertEqual(len(service.containers()), 1) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill', '-s', 'SIGINT'], None) + + self.assertEqual(len(service.containers()), 1) + # The container is still running. It has been only interrupted + self.assertTrue(service.containers()[0].is_running) + + def test_kill_interrupted_service(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.command.dispatch(['kill', '-s', 'SIGINT'], None) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill', '-s', 'SIGKILL'], None) + + self.assertEqual(len(service.containers(stopped=True)), 1) + self.assertFalse(service.containers(stopped=True)[0].is_running) + def test_restart(self): service = self.project.get_service('simple') container = service.create_container() diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index f1d1c79d910..119b4144062 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -13,6 +13,7 @@ from fig.service import ( ConfigError, split_port, + build_port_bindings, parse_volume_spec, build_volume_binding, APIError, @@ -114,6 +115,19 @@ def test_split_port_invalid(self): with self.assertRaises(ConfigError): split_port("0.0.0.0:1000:2000:tcp") + def test_build_port_bindings_with_one_port(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000")]) + + def test_build_port_bindings_with_matching_internal_ports(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000","127.0.0.1:2000:1000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000"),("127.0.0.1","2000")]) + + def test_build_port_bindings_with_nonmatching_internal_ports(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000","127.0.0.1:2000:2000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000")]) + self.assertEqual(port_bindings["2000"],[("127.0.0.1","2000")]) + def test_split_domainname_none(self): service = Service('foo', hostname='name', client=self.mock_client) self.mock_client.containers.return_value = []