compose/fig/cli/main.py

355 lines
12 KiB
Python
Raw Normal View History

2014-01-06 04:07:44 +01:00
from __future__ import print_function
2014-01-06 04:52:56 +01:00
from __future__ import unicode_literals
2013-12-11 15:25:32 +01:00
import logging
2013-12-19 14:06:26 +01:00
import sys
2013-12-11 15:25:32 +01:00
import re
import signal
2013-12-11 15:25:32 +01:00
from inspect import getdoc
import dockerpty
2013-12-11 15:25:32 +01:00
from .. import __version__
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError
2013-12-11 15:25:32 +01:00
from .command import Command
2013-12-19 14:02:04 +01:00
from .formatter import Formatter
2013-12-18 15:58:58 +01:00
from .log_printer import LogPrinter
2013-12-31 14:42:58 +01:00
from .utils import yesno
2013-12-11 15:25:32 +01:00
2014-04-23 19:20:27 +02:00
from ..packages.docker.errors import APIError
2013-12-11 15:25:32 +01:00
from .errors import UserError
from .docopt_command import NoSuchCommand
log = logging.getLogger(__name__)
2013-12-13 20:19:44 +01:00
2013-12-11 15:25:32 +01:00
def main():
2014-01-02 19:30:47 +01:00
console_handler = logging.StreamHandler(stream=sys.stderr)
2013-12-13 20:19:44 +01:00
console_handler.setFormatter(logging.Formatter())
console_handler.setLevel(logging.INFO)
root_logger = logging.getLogger()
root_logger.addHandler(console_handler)
root_logger.setLevel(logging.DEBUG)
# Disable requests logging
logging.getLogger("requests").propagate = False
2013-12-11 15:25:32 +01:00
try:
command = TopLevelCommand()
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
2014-02-19 23:42:21 +01:00
sys.exit(1)
except (UserError, NoSuchService, ConfigurationError) as e:
log.error(e.msg)
2014-02-19 23:42:21 +01:00
sys.exit(1)
2014-01-06 04:06:12 +01:00
except NoSuchCommand as e:
2013-12-11 15:25:32 +01:00
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
2014-02-19 23:42:21 +01:00
sys.exit(1)
2014-01-06 04:06:12 +01:00
except APIError as e:
log.error(e.explanation)
2014-02-19 23:42:21 +01:00
sys.exit(1)
except BuildError as e:
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
sys.exit(1)
2013-12-11 15:25:32 +01:00
# stolen from docopt master
def parse_doc_section(name, source):
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
re.IGNORECASE | re.MULTILINE)
return [s.strip() for s in pattern.findall(source)]
class TopLevelCommand(Command):
2013-12-20 22:34:27 +01:00
"""Punctual, lightweight development environments using Docker.
2013-12-11 15:25:32 +01:00
Usage:
2013-12-20 21:28:24 +01:00
fig [options] [COMMAND] [ARGS...]
fig -h|--help
2013-12-11 15:25:32 +01:00
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)
2013-12-11 15:25:32 +01:00
Commands:
2014-01-02 16:28:33 +01:00
build Build or rebuild services
2014-01-16 14:24:43 +01:00
help Get help on a command
2013-12-31 18:05:20 +01:00
kill Kill containers
2013-12-20 11:57:28 +01:00
logs View output from containers
2013-12-20 20:15:12 +01:00
ps List containers
2013-12-31 18:05:20 +01:00
rm Remove stopped containers
2013-12-13 21:55:28 +01:00
run Run a one-off command
2014-01-16 18:58:53 +01:00
scale Set number of containers for a service
2013-12-13 21:35:54 +01:00
start Start services
stop Stop services
2013-12-31 18:05:20 +01:00
up Create and start containers
2013-12-11 15:25:32 +01:00
"""
2013-12-19 14:06:26 +01:00
def docopt_options(self):
options = super(TopLevelCommand, self).docopt_options()
2013-12-20 21:28:24 +01:00
options['version'] = "fig %s" % __version__
2013-12-19 14:06:26 +01:00
return options
2014-01-02 16:28:33 +01:00
def build(self, options):
"""
Build or rebuild services.
Services are built once and then tagged as `project_service`,
e.g. `figtest_db`. If you change a service's `Dockerfile` or the
contents of its build directory, you can run `fig build` to rebuild it.
2014-01-02 16:28:33 +01:00
Usage: build [SERVICE...]
"""
self.project.build(service_names=options['SERVICE'])
2014-01-16 14:24:43 +01:00
def help(self, options):
"""
Get help on a command.
Usage: help COMMAND
"""
command = options['COMMAND']
if not hasattr(self, command):
raise NoSuchCommand(command, self)
raise SystemExit(getdoc(getattr(self, command)))
2013-12-31 18:05:20 +01:00
def kill(self, options):
"""
Force stop service containers.
2013-12-31 18:05:20 +01:00
Usage: kill [SERVICE...]
"""
self.project.kill(service_names=options['SERVICE'])
def logs(self, options):
"""
View output from containers.
Usage: logs [SERVICE...]
"""
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
2014-01-06 04:07:21 +01:00
print("Attaching to", list_containers(containers))
2013-12-31 18:05:20 +01:00
LogPrinter(containers, attach_params={'logs': True}).run()
2013-12-11 15:25:32 +01:00
def ps(self, options):
"""
2013-12-20 20:15:12 +01:00
List containers.
2013-12-11 15:25:32 +01:00
2013-12-20 20:13:55 +01:00
Usage: ps [options] [SERVICE...]
2013-12-19 14:02:04 +01:00
Options:
-q Only display IDs
2013-12-11 15:25:32 +01:00
"""
2013-12-20 20:13:55 +01:00
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
2013-12-19 14:02:04 +01:00
if options['-q']:
for container in containers:
2014-01-06 04:07:21 +01:00
print(container.id)
2013-12-19 14:02:04 +01:00
else:
headers = [
'Name',
'Command',
'State',
'Ports',
]
rows = []
for container in containers:
2014-01-16 15:02:52 +01:00
command = container.human_readable_command
if len(command) > 30:
command = '%s ...' % command[:26]
2013-12-19 14:02:04 +01:00
rows.append([
container.name,
2014-01-16 15:02:52 +01:00
command,
2013-12-19 14:02:04 +01:00
container.human_readable_state,
container.human_readable_ports,
])
2014-01-06 04:07:21 +01:00
print(Formatter().table(headers, rows))
2013-12-11 15:25:32 +01:00
2013-12-31 18:05:20 +01:00
def rm(self, options):
"""
Remove stopped service containers.
2013-12-31 18:05:20 +01:00
2014-03-04 11:25:50 +01:00
Usage: rm [options] [SERVICE...]
Options:
2014-03-04 11:25:50 +01:00
--force Don't ask to confirm removal
-v Remove volumes associated with containers
2013-12-31 18:05:20 +01:00
"""
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
stopped_containers = [c for c in all_containers if not c.is_running]
if len(stopped_containers) > 0:
2014-01-06 04:07:21 +01:00
print("Going to remove", list_containers(stopped_containers))
2014-03-04 11:25:50 +01:00
if options.get('--force') \
or yesno("Are you sure? [yN] ", default=False):
self.project.remove_stopped(
service_names=options['SERVICE'],
v=options.get('-v', False)
)
2013-12-31 18:05:20 +01:00
else:
2014-01-06 04:07:21 +01:00
print("No stopped containers")
2013-12-31 18:05:20 +01:00
2013-12-13 21:55:28 +01:00
def run(self, options):
"""
Run a one-off command on a service.
For example:
$ fig run web python manage.py shell
By default, linked services will be started, unless they are already
running. If you do not want to start linked services, use
`fig run --no-deps SERVICE COMMAND [ARGS...]`.
2013-12-13 21:55:28 +01:00
Usage: run [options] SERVICE [COMMAND] [ARGS...]
2013-12-20 11:53:07 +01:00
Options:
-d Detached mode: Run container in the background, print
new container name.
-T Disable pseudo-tty allocation. By default `fig run`
allocates a TTY.
--rm Remove container after run. Ignored in detached mode.
--no-deps Don't start linked services.
2013-12-13 21:55:28 +01:00
"""
service = self.project.get_service(options['SERVICE'])
if not options['--no-deps']:
deps = service.get_linked_names()
if len(deps) > 0:
self.project.up(
service_names=deps,
start_links=True,
recreate=False,
)
tty = True
if options['-d'] or options['-T'] or not sys.stdin.isatty():
tty = False
if options['COMMAND']:
command = [options['COMMAND']] + options['ARGS']
else:
command = service.options.get('command')
2013-12-17 15:13:12 +01:00
container_options = {
'command': command,
'tty': tty,
2013-12-20 16:03:01 +01:00
'stdin_open': not options['-d'],
2013-12-17 15:13:12 +01:00
}
container = service.create_container(one_off=True, **container_options)
2013-12-20 11:53:07 +01:00
if options['-d']:
service.start_container(container, ports=None, one_off=True)
2014-01-06 04:07:21 +01:00
print(container.name)
2013-12-20 11:53:07 +01:00
else:
service.start_container(container, ports=None, one_off=True)
dockerpty.start(self.client, container.id)
exit_code = container.wait()
if options['--rm']:
log.info("Removing %s..." % container.name)
self.client.remove_container(container.id)
sys.exit(exit_code)
2013-12-13 21:55:28 +01:00
2014-01-16 18:58:53 +01:00
def scale(self, options):
"""
Set number of containers to run for a service.
Numbers are specified in the form `service=num` as arguments.
For example:
$ fig scale web=2 worker=3
Usage: scale [SERVICE=NUM...]
"""
for s in options['SERVICE=NUM']:
if '=' not in s:
raise UserError('Arguments to scale should be in the form service=num')
service_name, num = s.split('=', 1)
try:
num = int(num)
except ValueError:
raise UserError('Number of containers for service "%s" is not a number' % service)
try:
self.project.get_service(service_name).scale(num)
except CannotBeScaledError:
raise UserError('Service "%s" cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash.\n\nRemove the ":" from the port definition in fig.yml so Docker can choose a random port for each container.' % service_name)
2013-12-31 18:05:20 +01:00
def start(self, options):
"""
Start existing containers.
Usage: start [SERVICE...]
"""
self.project.start(service_names=options['SERVICE'])
def stop(self, options):
"""
Stop running containers without removing them.
They can be started again with `fig start`.
2013-12-31 18:05:20 +01:00
Usage: stop [SERVICE...]
"""
self.project.stop(service_names=options['SERVICE'])
def up(self, options):
2013-12-11 15:25:32 +01:00
"""
Build, (re)create, start and attach to containers for a service.
By default, `fig up` will aggregate the output of each container, and
when it exits, all containers will be stopped. If you run `fig up -d`,
it'll start the containers in the background and leave them running.
If there are existing containers for a service, `fig up` will stop
and recreate them (preserving mounted volumes with volumes-from),
so that changes in `fig.yml` are picked up. If you do not want existing
containers to be recreated, `fig up --no-recreate` will re-use existing
containers.
2013-12-11 15:25:32 +01:00
Usage: up [options] [SERVICE...]
2013-12-20 11:53:07 +01:00
Options:
-d Detached mode: Run containers in the background,
print new container names.
--no-deps Don't start linked services.
--no-recreate If containers already exist, don't recreate them.
2013-12-11 15:25:32 +01:00
"""
detached = options['-d']
start_links = not options['--no-deps']
recreate = not options['--no-recreate']
service_names = options['SERVICE']
self.project.up(
service_names=service_names,
start_links=start_links,
recreate=recreate
)
to_attach = [c for s in self.project.get_services(service_names) for c in s.containers()]
if not detached:
print("Attaching to", list_containers(to_attach))
log_printer = LogPrinter(to_attach, attach_params={"logs": True})
try:
log_printer.run()
finally:
def handler(signal, frame):
self.project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
2014-01-06 04:07:21 +01:00
print("Gracefully stopping... (press Ctrl+C again to force)")
self.project.stop(service_names=service_names)
def list_containers(containers):
return ", ".join(c.name for c in containers)