123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- from __future__ import absolute_import
- from __future__ import print_function
- from __future__ import unicode_literals
- import contextlib
- import functools
- import json
- import logging
- import re
- import sys
- from inspect import getdoc
- from operator import attrgetter
- from . import errors
- from . import signals
- from .. import __version__
- from ..config import config
- from ..config import ConfigurationError
- from ..config import parse_environment
- from ..config.serialize import serialize_config
- from ..const import DEFAULT_TIMEOUT
- from ..const import IS_WINDOWS_PLATFORM
- from ..progress_stream import StreamOutputError
- from ..project import NoSuchService
- from ..service import BuildAction
- from ..service import BuildError
- from ..service import ConvergenceStrategy
- from ..service import ImageType
- from ..service import NeedsBuildError
- from .command import get_config_path_from_options
- from .command import project_from_options
- from .docopt_command import DocoptDispatcher
- from .docopt_command import get_handler
- from .docopt_command import NoSuchCommand
- from .errors import UserError
- from .formatter import ConsoleWarningFormatter
- from .formatter import Formatter
- from .log_printer import build_log_presenters
- from .log_printer import LogPrinter
- from .utils import get_version_info
- from .utils import yesno
- if not IS_WINDOWS_PLATFORM:
- from dockerpty.pty import PseudoTerminal, RunOperation, ExecOperation
- log = logging.getLogger(__name__)
- console_handler = logging.StreamHandler(sys.stderr)
- def main():
- command = dispatch()
- try:
- command()
- except (KeyboardInterrupt, signals.ShutdownException):
- log.error("Aborting.")
- sys.exit(1)
- except (UserError, NoSuchService, ConfigurationError) as e:
- log.error(e.msg)
- sys.exit(1)
- except BuildError as e:
- log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
- sys.exit(1)
- except StreamOutputError as e:
- log.error(e)
- sys.exit(1)
- except NeedsBuildError as e:
- log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
- sys.exit(1)
- except errors.ConnectionError:
- sys.exit(1)
- def dispatch():
- setup_logging()
- dispatcher = DocoptDispatcher(
- TopLevelCommand,
- {'options_first': True, 'version': get_version_info('compose')})
- try:
- options, handler, command_options = dispatcher.parse(sys.argv[1:])
- except NoSuchCommand as e:
- commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
- log.error("No such command: %s\n\n%s", e.command, commands)
- sys.exit(1)
- setup_console_handler(console_handler, options.get('--verbose'))
- return functools.partial(perform_command, options, handler, command_options)
- def perform_command(options, handler, command_options):
- if options['COMMAND'] in ('help', 'version'):
- # Skip looking up the compose file.
- handler(command_options)
- return
- if options['COMMAND'] == 'config':
- command = TopLevelCommand(None)
- handler(command, options, command_options)
- return
- project = project_from_options('.', options)
- command = TopLevelCommand(project)
- with errors.handle_connection_errors(project.client):
- handler(command, command_options)
- def setup_logging():
- root_logger = logging.getLogger()
- root_logger.addHandler(console_handler)
- root_logger.setLevel(logging.DEBUG)
- # Disable requests logging
- logging.getLogger("requests").propagate = False
- def setup_console_handler(handler, verbose):
- if handler.stream.isatty():
- format_class = ConsoleWarningFormatter
- else:
- format_class = logging.Formatter
- if verbose:
- handler.setFormatter(format_class('%(name)s.%(funcName)s: %(message)s'))
- handler.setLevel(logging.DEBUG)
- else:
- handler.setFormatter(format_class())
- handler.setLevel(logging.INFO)
- # 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(object):
- """Define and run multi-container applications with Docker.
- Usage:
- docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]
- docker-compose -h|--help
- Options:
- -f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
- -p, --project-name NAME Specify an alternate project name (default: directory name)
- --verbose Show more output
- -v, --version Print version and exit
- Commands:
- build Build or rebuild services
- config Validate and view the compose file
- create Create services
- down Stop and remove containers, networks, images, and volumes
- events Receive real time events from containers
- exec Execute a command in a running container
- help Get help on a command
- kill Kill containers
- logs View output from containers
- pause Pause services
- port Print the public port for a port binding
- ps List containers
- pull Pulls service images
- restart Restart services
- rm Remove stopped containers
- run Run a one-off command
- scale Set number of containers for a service
- start Start services
- stop Stop services
- unpause Unpause services
- up Create and start containers
- version Show the Docker-Compose version information
- """
- def __init__(self, project, project_dir='.'):
- self.project = project
- self.project_dir = '.'
- def build(self, options):
- """
- Build or rebuild services.
- Services are built once and then tagged as `project_service`,
- e.g. `composetest_db`. If you change a service's `Dockerfile` or the
- contents of its build directory, you can run `docker-compose build` to rebuild it.
- Usage: build [options] [SERVICE...]
- Options:
- --force-rm Always remove intermediate containers.
- --no-cache Do not use cache when building the image.
- --pull Always attempt to pull a newer version of the image.
- """
- self.project.build(
- service_names=options['SERVICE'],
- no_cache=bool(options.get('--no-cache', False)),
- pull=bool(options.get('--pull', False)),
- force_rm=bool(options.get('--force-rm', False)))
- def config(self, config_options, options):
- """
- Validate and view the compose file.
- Usage: config [options]
- Options:
- -q, --quiet Only validate the configuration, don't print
- anything.
- --services Print the service names, one per line.
- """
- config_path = get_config_path_from_options(config_options)
- compose_config = config.load(config.find(self.project_dir, config_path))
- if options['--quiet']:
- return
- if options['--services']:
- print('\n'.join(service['name'] for service in compose_config.services))
- return
- print(serialize_config(compose_config))
- def create(self, options):
- """
- Creates containers for a service.
- Usage: create [options] [SERVICE...]
- Options:
- --force-recreate Recreate containers even if their configuration and
- image haven't changed. Incompatible with --no-recreate.
- --no-recreate If containers already exist, don't recreate them.
- Incompatible with --force-recreate.
- --no-build Don't build an image, even if it's missing.
- --build Build images before creating containers.
- """
- service_names = options['SERVICE']
- self.project.create(
- service_names=service_names,
- strategy=convergence_strategy_from_opts(options),
- do_build=build_action_from_opts(options),
- )
- def down(self, options):
- """
- Stop containers and remove containers, networks, volumes, and images
- created by `up`. Only containers and networks are removed by default.
- Usage: down [options]
- Options:
- --rmi type Remove images, type may be one of: 'all' to remove
- all images, or 'local' to remove only images that
- don't have an custom name set by the `image` field
- -v, --volumes Remove data volumes
- """
- image_type = image_type_from_opt('--rmi', options['--rmi'])
- self.project.down(image_type, options['--volumes'])
- def events(self, options):
- """
- Receive real time events from containers.
- Usage: events [options] [SERVICE...]
- Options:
- --json Output events as a stream of json objects
- """
- def format_event(event):
- attributes = ["%s=%s" % item for item in event['attributes'].items()]
- return ("{time} {type} {action} {id} ({attrs})").format(
- attrs=", ".join(sorted(attributes)),
- **event)
- def json_format_event(event):
- event['time'] = event['time'].isoformat()
- event.pop('container')
- return json.dumps(event)
- for event in self.project.events():
- formatter = json_format_event if options['--json'] else format_event
- print(formatter(event))
- sys.stdout.flush()
- def exec_command(self, options):
- """
- Execute a command in a running container
- Usage: exec [options] SERVICE COMMAND [ARGS...]
- Options:
- -d Detached mode: Run command in the background.
- --privileged Give extended privileges to the process.
- --user USER Run the command as this user.
- -T Disable pseudo-tty allocation. By default `docker-compose exec`
- allocates a TTY.
- --index=index index of the container if there are multiple
- instances of a service [default: 1]
- """
- index = int(options.get('--index'))
- service = self.project.get_service(options['SERVICE'])
- try:
- container = service.get_container(number=index)
- except ValueError as e:
- raise UserError(str(e))
- command = [options['COMMAND']] + options['ARGS']
- tty = not options["-T"]
- create_exec_options = {
- "privileged": options["--privileged"],
- "user": options["--user"],
- "tty": tty,
- "stdin": tty,
- }
- exec_id = container.create_exec(command, **create_exec_options)
- if options['-d']:
- container.start_exec(exec_id, tty=tty)
- return
- signals.set_signal_handler_to_shutdown()
- try:
- operation = ExecOperation(
- self.project.client,
- exec_id,
- interactive=tty,
- )
- pty = PseudoTerminal(self.project.client, operation)
- pty.start()
- except signals.ShutdownException:
- log.info("received shutdown exception: closing")
- exit_code = self.project.client.exec_inspect(exec_id).get("ExitCode")
- sys.exit(exit_code)
- @classmethod
- def help(cls, options):
- """
- Get help on a command.
- Usage: help COMMAND
- """
- handler = get_handler(cls, options['COMMAND'])
- raise SystemExit(getdoc(handler))
- def kill(self, options):
- """
- Force stop service containers.
- Usage: kill [options] [SERVICE...]
- Options:
- -s SIGNAL SIGNAL to send to the container.
- Default signal is SIGKILL.
- """
- signal = options.get('-s', 'SIGKILL')
- self.project.kill(service_names=options['SERVICE'], signal=signal)
- def logs(self, options):
- """
- View output from containers.
- Usage: logs [options] [SERVICE...]
- Options:
- --no-color Produce monochrome output.
- -f, --follow Follow log output.
- -t, --timestamps Show timestamps.
- --tail="all" Number of lines to show from the end of the logs
- for each container.
- """
- containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
- tail = options['--tail']
- if tail is not None:
- if tail.isdigit():
- tail = int(tail)
- elif tail != 'all':
- raise UserError("tail flag must be all or a number")
- log_args = {
- 'follow': options['--follow'],
- 'tail': tail,
- 'timestamps': options['--timestamps']
- }
- print("Attaching to", list_containers(containers))
- log_printer_from_project(
- project,
- containers,
- options['--no-color'],
- log_args).run()
- def pause(self, options):
- """
- Pause services.
- Usage: pause [SERVICE...]
- """
- containers = self.project.pause(service_names=options['SERVICE'])
- exit_if(not containers, 'No containers to pause', 1)
- def port(self, options):
- """
- Print the public port for a port binding.
- Usage: port [options] SERVICE PRIVATE_PORT
- Options:
- --protocol=proto tcp or udp [default: tcp]
- --index=index index of the container if there are multiple
- instances of a service [default: 1]
- """
- index = int(options.get('--index'))
- service = self.project.get_service(options['SERVICE'])
- try:
- container = service.get_container(number=index)
- except ValueError as e:
- raise UserError(str(e))
- print(container.get_local_port(
- options['PRIVATE_PORT'],
- protocol=options.get('--protocol') or 'tcp') or '')
- def ps(self, options):
- """
- List containers.
- Usage: ps [options] [SERVICE...]
- Options:
- -q Only display IDs
- """
- containers = sorted(
- self.project.containers(service_names=options['SERVICE'], stopped=True) +
- self.project.containers(service_names=options['SERVICE'], one_off=True),
- key=attrgetter('name'))
- 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 pull(self, options):
- """
- Pulls images for services.
- Usage: pull [options] [SERVICE...]
- Options:
- --ignore-pull-failures Pull what it can and ignores images with pull failures.
- """
- self.project.pull(
- service_names=options['SERVICE'],
- ignore_pull_failures=options.get('--ignore-pull-failures')
- )
- def rm(self, options):
- """
- Remove stopped service containers.
- By default, volumes attached to containers will not be removed. You can see all
- volumes with `docker volume ls`.
- Any data which is not in a volume will be lost.
- Usage: rm [options] [SERVICE...]
- Options:
- -f, --force Don't ask to confirm removal
- -v Remove volumes associated with containers
- """
- 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:
- print("Going to remove", list_containers(stopped_containers))
- 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)
- )
- else:
- print("No stopped containers")
- def run(self, options):
- """
- Run a one-off command on a service.
- For example:
- $ docker-compose 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
- `docker-compose run --no-deps SERVICE COMMAND [ARGS...]`.
- Usage: run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
- Options:
- -d Detached mode: Run container in the background, print
- new container name.
- --name NAME Assign a name to the container
- --entrypoint CMD Override the entrypoint of the image.
- -e KEY=VAL Set an environment variable (can be used multiple times)
- -u, --user="" Run as specified username or uid
- --no-deps Don't start linked services.
- --rm Remove container after run. Ignored in detached mode.
- -p, --publish=[] Publish a container's port(s) to the host
- --service-ports Run command with the service's ports enabled and mapped
- to the host.
- -T Disable pseudo-tty allocation. By default `docker-compose run`
- allocates a TTY.
- """
- service = self.project.get_service(options['SERVICE'])
- detach = options['-d']
- if IS_WINDOWS_PLATFORM and not detach:
- raise UserError(
- "Interactive mode is not yet supported on Windows.\n"
- "Please pass the -d flag when using `docker-compose run`."
- )
- if options['COMMAND']:
- command = [options['COMMAND']] + options['ARGS']
- else:
- command = service.options.get('command')
- container_options = {
- 'command': command,
- 'tty': not (detach or options['-T'] or not sys.stdin.isatty()),
- 'stdin_open': not detach,
- 'detach': detach,
- }
- if options['-e']:
- container_options['environment'] = parse_environment(options['-e'])
- if options['--entrypoint']:
- container_options['entrypoint'] = options.get('--entrypoint')
- if options['--rm']:
- container_options['restart'] = None
- if options['--user']:
- container_options['user'] = options.get('--user')
- if not options['--service-ports']:
- container_options['ports'] = []
- if options['--publish']:
- container_options['ports'] = options.get('--publish')
- if options['--publish'] and options['--service-ports']:
- raise UserError(
- 'Service port mapping and manual port mapping '
- 'can not be used togather'
- )
- if options['--name']:
- container_options['name'] = options['--name']
- run_one_off_container(container_options, self.project, service, options)
- 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:
- $ docker-compose scale web=2 worker=3
- Usage: scale [options] [SERVICE=NUM...]
- Options:
- -t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
- (default: 10)
- """
- timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
- 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)
- self.project.get_service(service_name).scale(num, timeout=timeout)
- def start(self, options):
- """
- Start existing containers.
- Usage: start [SERVICE...]
- """
- containers = self.project.start(service_names=options['SERVICE'])
- exit_if(not containers, 'No containers to start', 1)
- def stop(self, options):
- """
- Stop running containers without removing them.
- They can be started again with `docker-compose start`.
- Usage: stop [options] [SERVICE...]
- Options:
- -t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
- (default: 10)
- """
- timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
- self.project.stop(service_names=options['SERVICE'], timeout=timeout)
- def restart(self, options):
- """
- Restart running containers.
- Usage: restart [options] [SERVICE...]
- Options:
- -t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
- (default: 10)
- """
- timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
- containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
- exit_if(not containers, 'No containers to restart', 1)
- def unpause(self, options):
- """
- Unpause services.
- Usage: unpause [SERVICE...]
- """
- containers = self.project.unpause(service_names=options['SERVICE'])
- exit_if(not containers, 'No containers to unpause', 1)
- def up(self, options):
- """
- Builds, (re)creates, starts, and attaches to containers for a service.
- Unless they are already running, this command also starts any linked services.
- The `docker-compose up` command aggregates the output of each container. When
- the command exits, all containers are stopped. Running `docker-compose up -d`
- starts the containers in the background and leaves them running.
- If there are existing containers for a service, and the service's configuration
- or image was changed after the container's creation, `docker-compose up` picks
- up the changes by stopping and recreating the containers (preserving mounted
- volumes). To prevent Compose from picking up changes, use the `--no-recreate`
- flag.
- If you want to force Compose to stop and recreate all containers, use the
- `--force-recreate` flag.
- Usage: up [options] [SERVICE...]
- Options:
- -d Detached mode: Run containers in the background,
- print new container names.
- Incompatible with --abort-on-container-exit.
- --no-color Produce monochrome output.
- --no-deps Don't start linked services.
- --force-recreate Recreate containers even if their configuration
- and image haven't changed.
- Incompatible with --no-recreate.
- --no-recreate If containers already exist, don't recreate them.
- Incompatible with --force-recreate.
- --no-build Don't build an image, even if it's missing.
- --build Build images before starting containers.
- --abort-on-container-exit Stops all containers if any container was stopped.
- Incompatible with -d.
- -t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
- when attached or when containers are already
- running. (default: 10)
- """
- start_deps = not options['--no-deps']
- cascade_stop = options['--abort-on-container-exit']
- service_names = options['SERVICE']
- timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
- detached = options.get('-d')
- if detached and cascade_stop:
- raise UserError("--abort-on-container-exit and -d cannot be combined.")
- with up_shutdown_context(self.project, service_names, timeout, detached):
- # start the event stream first so we don't lose any events
- event_stream = project.events()
- to_attach = project.up(
- service_names=service_names,
- start_deps=start_deps,
- strategy=convergence_strategy_from_opts(options),
- do_build=build_action_from_opts(options),
- timeout=timeout,
- detached=detached)
- if detached:
- return
- log_printer = log_printer_from_project(
- project,
- filter_containers_to_service_names(to_attach, service_names),
- options['--no-color'],
- {'follow': True},
- cascade_stop,
- event_stream=event_stream)
- print("Attaching to", list_containers(log_printer.containers))
- log_printer.run()
- if cascade_stop:
- print("Aborting on container exit...")
- self.project.stop(service_names=service_names, timeout=timeout)
- @classmethod
- def version(cls, options):
- """
- Show version informations
- Usage: version [--short]
- Options:
- --short Shows only Compose's version number.
- """
- if options['--short']:
- print(__version__)
- else:
- print(get_version_info('full'))
- def convergence_strategy_from_opts(options):
- no_recreate = options['--no-recreate']
- force_recreate = options['--force-recreate']
- if force_recreate and no_recreate:
- raise UserError("--force-recreate and --no-recreate cannot be combined.")
- if force_recreate:
- return ConvergenceStrategy.always
- if no_recreate:
- return ConvergenceStrategy.never
- return ConvergenceStrategy.changed
- def image_type_from_opt(flag, value):
- if not value:
- return ImageType.none
- try:
- return ImageType[value]
- except KeyError:
- raise UserError("%s flag must be one of: all, local" % flag)
- def build_action_from_opts(options):
- if options['--build'] and options['--no-build']:
- raise UserError("--build and --no-build can not be combined.")
- if options['--build']:
- return BuildAction.force
- if options['--no-build']:
- return BuildAction.skip
- return BuildAction.none
- def run_one_off_container(container_options, project, service, options):
- if not options['--no-deps']:
- deps = service.get_dependency_names()
- if deps:
- project.up(
- service_names=deps,
- start_deps=True,
- strategy=ConvergenceStrategy.never)
- project.initialize()
- container = service.create_container(
- quiet=True,
- one_off=True,
- **container_options)
- if options['-d']:
- service.start_container(container)
- print(container.name)
- return
- def remove_container(force=False):
- if options['--rm']:
- project.client.remove_container(container.id, force=True)
- signals.set_signal_handler_to_shutdown()
- try:
- try:
- operation = RunOperation(
- project.client,
- container.id,
- interactive=not options['-T'],
- logs=False,
- )
- pty = PseudoTerminal(project.client, operation)
- sockets = pty.sockets()
- service.start_container(container)
- pty.start(sockets)
- exit_code = container.wait()
- except signals.ShutdownException:
- project.client.stop(container.id)
- exit_code = 1
- except signals.ShutdownException:
- project.client.kill(container.id)
- remove_container(force=True)
- sys.exit(2)
- remove_container()
- sys.exit(exit_code)
- def log_printer_from_project(
- project,
- containers,
- monochrome,
- log_args,
- cascade_stop=False,
- event_stream=None,
- ):
- return LogPrinter(
- containers,
- build_log_presenters(project.service_names, monochrome),
- event_stream or project.events(),
- cascade_stop=cascade_stop,
- log_args=log_args)
- def filter_containers_to_service_names(containers, service_names):
- if not service_names:
- return containers
- return [
- container
- for container in containers if container.service in service_names
- ]
- @contextlib.contextmanager
- def up_shutdown_context(project, service_names, timeout, detached):
- if detached:
- yield
- return
- signals.set_signal_handler_to_shutdown()
- try:
- try:
- yield
- except signals.ShutdownException:
- print("Gracefully stopping... (press Ctrl+C again to force)")
- project.stop(service_names=service_names, timeout=timeout)
- except signals.ShutdownException:
- project.kill(service_names=service_names)
- sys.exit(2)
- def list_containers(containers):
- return ", ".join(c.name for c in containers)
- def exit_if(condition, message, exit_code):
- if condition:
- log.error(message)
- raise SystemExit(exit_code)
|