123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- from __future__ import print_function
- from __future__ import unicode_literals
- import logging
- import sys
- import re
- import signal
- from inspect import getdoc
- import dockerpty
- from .. import __version__
- from ..project import NoSuchService, ConfigurationError
- from ..service import BuildError, CannotBeScaledError
- from .command import Command
- from .formatter import Formatter
- from .log_printer import LogPrinter
- from .utils import yesno
- from ..packages.docker.errors import APIError
- from .errors import UserError
- from .docopt_command import NoSuchCommand
- log = logging.getLogger(__name__)
- def main():
- setup_logging()
- try:
- command = TopLevelCommand()
- command.sys_dispatch()
- except KeyboardInterrupt:
- log.error("\nAborting.")
- sys.exit(1)
- except (UserError, NoSuchService, ConfigurationError) as e:
- log.error(e.msg)
- sys.exit(1)
- except NoSuchCommand as e:
- log.error("No such command: %s", e.command)
- log.error("")
- log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
- sys.exit(1)
- except APIError as e:
- log.error(e.explanation)
- sys.exit(1)
- except BuildError as e:
- log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
- sys.exit(1)
- def setup_logging():
- console_handler = logging.StreamHandler(sys.stderr)
- 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
- # 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):
- """Punctual, lightweight development environments using Docker.
- Usage:
- fig [options] [COMMAND] [ARGS...]
- fig -h|--help
- 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 Build or rebuild services
- help Get help on a command
- kill Kill containers
- logs View output from containers
- ps List containers
- rm Remove stopped containers
- run Run a one-off command
- scale Set number of containers for a service
- start Start services
- stop Stop services
- up Create and start containers
- """
- def docopt_options(self):
- options = super(TopLevelCommand, self).docopt_options()
- options['version'] = "fig %s" % __version__
- return options
- def build(self, project, 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.
- Usage: build [options] [SERVICE...]
- Options:
- --no-cache Do not use cache when building the image.
- """
- no_cache = bool(options.get('--no-cache', False))
- project.build(service_names=options['SERVICE'], no_cache=no_cache)
- def help(self, project, 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)))
- def kill(self, project, options):
- """
- Force stop service containers.
- Usage: kill [SERVICE...]
- """
- project.kill(service_names=options['SERVICE'])
- def logs(self, project, options):
- """
- View output from containers.
- Usage: logs [options] [SERVICE...]
- Options:
- --no-color Produce monochrome output.
- """
- containers = project.containers(service_names=options['SERVICE'], stopped=True)
- monochrome = options['--no-color']
- print("Attaching to", list_containers(containers))
- LogPrinter(containers, attach_params={'logs': True}, monochrome=monochrome).run()
- def ps(self, project, options):
- """
- List containers.
- Usage: ps [options] [SERVICE...]
- Options:
- -q Only display IDs
- """
- containers = project.containers(service_names=options['SERVICE'], stopped=True) + project.containers(service_names=options['SERVICE'], one_off=True)
- if options['-q']:
- for container in containers:
- print(container.id)
- else:
- headers = [
- 'Name',
- 'Command',
- 'State',
- 'Ports',
- ]
- rows = []
- for container in containers:
- command = container.human_readable_command
- if len(command) > 30:
- command = '%s ...' % command[:26]
- rows.append([
- container.name,
- command,
- container.human_readable_state,
- container.human_readable_ports,
- ])
- print(Formatter().table(headers, rows))
- def rm(self, project, options):
- """
- Remove stopped service containers.
- Usage: rm [options] [SERVICE...]
- Options:
- --force Don't ask to confirm removal
- -v Remove volumes associated with containers
- """
- all_containers = 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:
- print("Going to remove", list_containers(stopped_containers))
- if options.get('--force') \
- or yesno("Are you sure? [yN] ", default=False):
- project.remove_stopped(
- service_names=options['SERVICE'],
- v=options.get('-v', False)
- )
- else:
- print("No stopped containers")
- def run(self, project, 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...]`.
- Usage: run [options] SERVICE [COMMAND] [ARGS...]
- 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.
- """
- service = project.get_service(options['SERVICE'])
- if not options['--no-deps']:
- deps = service.get_linked_names()
- if len(deps) > 0:
- 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')
- container_options = {
- 'command': command,
- 'tty': tty,
- 'stdin_open': not options['-d'],
- }
- container = service.create_container(one_off=True, **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)
- exit_code = container.wait()
- if options['--rm']:
- log.info("Removing %s..." % container.name)
- project.client.remove_container(container.id)
- sys.exit(exit_code)
- def scale(self, project, 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_name)
- try:
- 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)
- def start(self, project, options):
- """
- Start existing containers.
- Usage: start [SERVICE...]
- """
- project.start(service_names=options['SERVICE'])
- def stop(self, project, options):
- """
- Stop running containers without removing them.
- They can be started again with `fig start`.
- Usage: stop [SERVICE...]
- """
- project.stop(service_names=options['SERVICE'])
- def up(self, project, options):
- """
- 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.
- 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.
- """
- detached = options['-d']
- monochrome = options['--no-color']
- start_links = not options['--no-deps']
- recreate = not options['--no-recreate']
- service_names = options['SERVICE']
- project.up(
- service_names=service_names,
- start_links=start_links,
- recreate=recreate
- )
- to_attach = [c for s in 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}, monochrome=monochrome)
- try:
- log_printer.run()
- finally:
- def handler(signal, frame):
- project.kill(service_names=service_names)
- sys.exit(0)
- signal.signal(signal.SIGINT, handler)
- print("Gracefully stopping... (press Ctrl+C again to force)")
- project.stop(service_names=service_names)
- def list_containers(containers):
- return ", ".join(c.name for c in containers)
|