12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574 |
- from __future__ import absolute_import
- from __future__ import print_function
- from __future__ import unicode_literals
- import contextlib
- import functools
- import json
- import logging
- import pipes
- import re
- import subprocess
- import sys
- from distutils.spawn import find_executable
- from inspect import getdoc
- from operator import attrgetter
- import docker
- from . import errors
- from . import signals
- from .. import __version__
- from ..bundle import get_image_digests
- from ..bundle import MissingDigests
- from ..bundle import serialize_bundle
- from ..config import ConfigurationError
- from ..config import parse_environment
- from ..config import parse_labels
- from ..config import resolve_build_args
- from ..config.environment import Environment
- from ..config.serialize import serialize_config
- from ..config.types import VolumeSpec
- from ..const import COMPOSEFILE_V2_2 as V2_2
- from ..const import IS_WINDOWS_PLATFORM
- from ..errors import StreamParseError
- from ..progress_stream import StreamOutputError
- from ..project import NoSuchService
- from ..project import OneOffFilter
- from ..project import ProjectError
- from ..service import BuildAction
- from ..service import BuildError
- from ..service import ConvergenceStrategy
- from ..service import ImageType
- from ..service import NeedsBuildError
- from ..service import OperationFailedError
- from .command import get_config_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 human_readable_file_size
- 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():
- signals.ignore_sigpipe()
- try:
- command = dispatch()
- command()
- except (KeyboardInterrupt, signals.ShutdownException):
- log.error("Aborting.")
- sys.exit(1)
- except (UserError, NoSuchService, ConfigurationError,
- ProjectError, OperationFailedError) 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 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)
- except (errors.ConnectionError, StreamParseError):
- sys.exit(1)
- def dispatch():
- setup_logging()
- dispatcher = DocoptDispatcher(
- TopLevelCommand,
- {'options_first': True, 'version': get_version_info('compose')})
- options, handler, command_options = dispatcher.parse(sys.argv[1:])
- setup_console_handler(console_handler,
- options.get('--verbose'),
- options.get('--no-ansi'),
- options.get("--log-level"))
- setup_parallel_logger(options.get('--no-ansi'))
- if options.get('--no-ansi'):
- command_options['--no-color'] = True
- 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, options=options)
- handler(command, command_options)
- return
- project = project_from_options('.', options)
- command = TopLevelCommand(project, options=options)
- 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_parallel_logger(noansi):
- if noansi:
- import compose.parallel
- compose.parallel.ParallelStreamWriter.set_noansi()
- def setup_console_handler(handler, verbose, noansi=False, level=None):
- if handler.stream.isatty() and noansi is False:
- format_class = ConsoleWarningFormatter
- else:
- format_class = logging.Formatter
- if verbose:
- handler.setFormatter(format_class('%(name)s.%(funcName)s: %(message)s'))
- loglevel = logging.DEBUG
- else:
- handler.setFormatter(format_class())
- loglevel = logging.INFO
- if level is not None:
- levels = {
- 'DEBUG': logging.DEBUG,
- 'INFO': logging.INFO,
- 'WARNING': logging.WARNING,
- 'ERROR': logging.ERROR,
- 'CRITICAL': logging.CRITICAL,
- }
- loglevel = levels.get(level.upper())
- if loglevel is None:
- raise UserError(
- 'Invalid value for --log-level. Expected one of DEBUG, INFO, WARNING, ERROR, CRITICAL.'
- )
- handler.setLevel(loglevel)
- # 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
- --log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- --no-ansi Do not print ANSI control characters
- -v, --version Print version and exit
- -H, --host HOST Daemon socket to connect to
- --tls Use TLS; implied by --tlsverify
- --tlscacert CA_PATH Trust certs signed only by this CA
- --tlscert CLIENT_CERT_PATH Path to TLS certificate file
- --tlskey TLS_KEY_PATH Path to TLS key file
- --tlsverify Use TLS and verify the remote
- --skip-hostname-check Don't check the daemon's hostname against the
- name specified in the client certificate
- --project-directory PATH Specify an alternate working directory
- (default: the path of the Compose file)
- --compatibility If set, Compose will attempt to convert keys
- in v3 files to their non-Swarm equivalent
- Commands:
- build Build or rebuild services
- bundle Generate a Docker bundle from the Compose file
- 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
- images List images
- kill Kill containers
- logs View output from containers
- pause Pause services
- port Print the public port for a port binding
- ps List containers
- pull Pull service images
- push Push 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
- top Display the running processes
- unpause Unpause services
- up Create and start containers
- version Show the Docker-Compose version information
- """
- def __init__(self, project, options=None):
- self.project = project
- self.toplevel_options = options or {}
- @property
- def project_dir(self):
- return self.toplevel_options.get('--project-directory') or '.'
- 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] [--build-arg key=val...] [SERVICE...]
- Options:
- --compress Compress the build context using gzip.
- --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.
- -m, --memory MEM Sets memory limit for the build container.
- --build-arg key=val Set build-time variables for services.
- --parallel Build images in parallel.
- """
- service_names = options['SERVICE']
- build_args = options.get('--build-arg', None)
- if build_args:
- if not service_names and docker.utils.version_lt(self.project.client.api_version, '1.25'):
- raise UserError(
- '--build-arg is only supported when services are specified for API version < 1.25.'
- ' Please use a Compose file version > 2.2 or specify which services to build.'
- )
- environment = Environment.from_env_file(self.project_dir)
- build_args = resolve_build_args(build_args, environment)
- 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)),
- memory=options.get('--memory'),
- build_args=build_args,
- gzip=options.get('--compress', False),
- parallel_build=options.get('--parallel', False),
- )
- def bundle(self, options):
- """
- Generate a Distributed Application Bundle (DAB) from the Compose file.
- Images must have digests stored, which requires interaction with a
- Docker registry. If digests aren't stored for all images, you can fetch
- them with `docker-compose pull` or `docker-compose push`. To push images
- automatically when bundling, pass `--push-images`. Only services with
- a `build` option specified will have their images pushed.
- Usage: bundle [options]
- Options:
- --push-images Automatically push images for any services
- which have a `build` option specified.
- -o, --output PATH Path to write the bundle file to.
- Defaults to "<project name>.dab".
- """
- compose_config = get_config_from_options('.', self.toplevel_options)
- output = options["--output"]
- if not output:
- output = "{}.dab".format(self.project.name)
- image_digests = image_digests_for_project(self.project, options['--push-images'])
- with open(output, 'w') as f:
- f.write(serialize_bundle(compose_config, image_digests))
- log.info("Wrote bundle to {}".format(output))
- def config(self, options):
- """
- Validate and view the Compose file.
- Usage: config [options]
- Options:
- --resolve-image-digests Pin image tags to digests.
- -q, --quiet Only validate the configuration, don't print
- anything.
- --services Print the service names, one per line.
- --volumes Print the volume names, one per line.
- --hash="*" Print the service config hash, one per line.
- Set "service1,service2" for a list of specified services
- or use the wildcard symbol to display all services
- """
- compose_config = get_config_from_options('.', self.toplevel_options)
- image_digests = None
- if options['--resolve-image-digests']:
- self.project = project_from_options('.', self.toplevel_options)
- with errors.handle_connection_errors(self.project.client):
- image_digests = image_digests_for_project(self.project)
- if options['--quiet']:
- return
- if options['--services']:
- print('\n'.join(service['name'] for service in compose_config.services))
- return
- if options['--volumes']:
- print('\n'.join(volume for volume in compose_config.volumes))
- return
- if options['--hash'] is not None:
- h = options['--hash']
- self.project = project_from_options('.', self.toplevel_options)
- services = [svc for svc in options['--hash'].split(',')] if h != '*' else None
- with errors.handle_connection_errors(self.project.client):
- for service in self.project.get_services(services):
- print('{} {}'.format(service.name, service.config_hash))
- return
- print(serialize_config(compose_config, image_digests))
- def create(self, options):
- """
- Creates containers for a service.
- This command is deprecated. Use the `up` command with `--no-start` instead.
- 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']
- log.warn(
- 'The create command is deprecated. '
- 'Use the up command with the --no-start flag instead.'
- )
- self.project.create(
- service_names=service_names,
- strategy=convergence_strategy_from_opts(options),
- do_build=build_action_from_opts(options),
- )
- def down(self, options):
- """
- Stops containers and removes containers, networks, volumes, and images
- created by `up`.
- By default, the only things removed are:
- - Containers for services defined in the Compose file
- - Networks defined in the `networks` section of the Compose file
- - The default network, if one is used
- Networks and volumes defined as `external` are never removed.
- Usage: down [options]
- Options:
- --rmi type Remove images. Type must be one of:
- 'all': Remove all images used by any service.
- 'local': Remove only images that don't have a
- custom tag set by the `image` field.
- -v, --volumes Remove named volumes declared in the `volumes`
- section of the Compose file and anonymous volumes
- attached to containers.
- --remove-orphans Remove containers for services not defined in the
- Compose file
- -t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
- (default: 10)
- """
- environment = Environment.from_env_file(self.project_dir)
- ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
- if ignore_orphans and options['--remove-orphans']:
- raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.")
- image_type = image_type_from_opt('--rmi', options['--rmi'])
- timeout = timeout_from_opts(options)
- self.project.down(
- image_type,
- options['--volumes'],
- options['--remove-orphans'],
- timeout=timeout,
- ignore_orphans=ignore_orphans)
- 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] [-e KEY=VAL...] SERVICE COMMAND [ARGS...]
- Options:
- -d, --detach Detached mode: Run command in the background.
- --privileged Give extended privileges to the process.
- -u, --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]
- -e, --env KEY=VAL Set environment variables (can be used multiple times,
- not supported in API < 1.25)
- -w, --workdir DIR Path to workdir directory for this command.
- """
- environment = Environment.from_env_file(self.project_dir)
- use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
- index = int(options.get('--index'))
- service = self.project.get_service(options['SERVICE'])
- detach = options.get('--detach')
- if options['--env'] and docker.utils.version_lt(self.project.client.api_version, '1.25'):
- raise UserError("Setting environment for exec is not supported in API < 1.25 (%s)"
- % self.project.client.api_version)
- if options['--workdir'] and docker.utils.version_lt(self.project.client.api_version, '1.35'):
- raise UserError("Setting workdir for exec is not supported in API < 1.35 (%s)"
- % self.project.client.api_version)
- try:
- container = service.get_container(number=index)
- except ValueError as e:
- raise UserError(str(e))
- command = [options['COMMAND']] + options['ARGS']
- tty = not options["-T"]
- if IS_WINDOWS_PLATFORM or use_cli and not detach:
- sys.exit(call_docker(
- build_exec_command(options, container.id, command),
- self.toplevel_options)
- )
- create_exec_options = {
- "privileged": options["--privileged"],
- "user": options["--user"],
- "tty": tty,
- "stdin": True,
- "workdir": options["--workdir"],
- }
- if docker.utils.version_gte(self.project.client.api_version, '1.25'):
- create_exec_options["environment"] = options["--env"]
- exec_id = container.create_exec(command, **create_exec_options)
- if detach:
- container.start_exec(exec_id, tty=tty, stream=True)
- 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]
- """
- if options['COMMAND']:
- subject = get_handler(cls, options['COMMAND'])
- else:
- subject = cls
- print(getdoc(subject))
- def images(self, options):
- """
- List images used by the created containers.
- Usage: images [options] [SERVICE...]
- Options:
- -q, --quiet Only display IDs
- """
- containers = sorted(
- self.project.containers(service_names=options['SERVICE'], stopped=True) +
- self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
- key=attrgetter('name'))
- if options['--quiet']:
- for image in set(c.image for c in containers):
- print(image.split(':')[1])
- return
- def add_default_tag(img_name):
- if ':' not in img_name.split('/')[-1]:
- return '{}:latest'.format(img_name)
- return img_name
- headers = [
- 'Container',
- 'Repository',
- 'Tag',
- 'Image Id',
- 'Size'
- ]
- rows = []
- for container in containers:
- image_config = container.image_config
- service = self.project.get_service(container.service)
- index = 0
- img_name = add_default_tag(service.image_name)
- if img_name in image_config['RepoTags']:
- index = image_config['RepoTags'].index(img_name)
- repo_tags = (
- image_config['RepoTags'][index].rsplit(':', 1) if image_config['RepoTags']
- else ('<none>', '<none>')
- )
- image_id = image_config['Id'].split(':')[1][:12]
- size = human_readable_file_size(image_config['Size'])
- rows.append([
- container.name,
- repo_tags[0],
- repo_tags[1],
- image_id,
- size
- ])
- print(Formatter().table(headers, rows))
- 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(
- self.project,
- containers,
- options['--no-color'],
- log_args,
- event_stream=self.project.events(service_names=options['SERVICE'])).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, --quiet Only display IDs
- --services Display services
- --filter KEY=VAL Filter services by a property
- -a, --all Show all stopped containers (including those created by the run command)
- """
- if options['--quiet'] and options['--services']:
- raise UserError('--quiet and --services cannot be combined')
- if options['--services']:
- filt = build_filter(options.get('--filter'))
- services = self.project.services
- if filt:
- services = filter_services(filt, services, self.project)
- print('\n'.join(service.name for service in services))
- return
- if options['--all']:
- containers = sorted(self.project.containers(service_names=options['SERVICE'],
- one_off=OneOffFilter.include, stopped=True))
- else:
- containers = sorted(
- self.project.containers(service_names=options['SERVICE'], stopped=True) +
- self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
- key=attrgetter('name'))
- if options['--quiet']:
- 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 defined in a Compose file, but does not start the containers.
- Usage: pull [options] [SERVICE...]
- Options:
- --ignore-pull-failures Pull what it can and ignores images with pull failures.
- --parallel Deprecated, pull multiple images in parallel (enabled by default).
- --no-parallel Disable parallel pulling.
- -q, --quiet Pull without printing progress information
- --include-deps Also pull services declared as dependencies
- """
- if options.get('--parallel'):
- log.warn('--parallel option is deprecated and will be removed in future versions.')
- self.project.pull(
- service_names=options['SERVICE'],
- ignore_pull_failures=options.get('--ignore-pull-failures'),
- parallel_pull=not options.get('--no-parallel'),
- silent=options.get('--quiet'),
- include_deps=options.get('--include-deps'),
- )
- def push(self, options):
- """
- Pushes images for services.
- Usage: push [options] [SERVICE...]
- Options:
- --ignore-push-failures Push what it can and ignores images with push failures.
- """
- self.project.push(
- service_names=options['SERVICE'],
- ignore_push_failures=options.get('--ignore-push-failures')
- )
- def rm(self, options):
- """
- Removes stopped service containers.
- By default, anonymous volumes attached to containers will not be removed. You
- can override this with `-v`. To list all volumes, use `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
- -s, --stop Stop the containers, if required, before removing
- -v Remove any anonymous volumes attached to containers
- -a, --all Deprecated - no effect.
- """
- if options.get('--all'):
- log.warn(
- '--all flag is obsolete. This is now the default behavior '
- 'of `docker-compose rm`'
- )
- one_off = OneOffFilter.include
- if options.get('--stop'):
- self.project.stop(service_names=options['SERVICE'], one_off=one_off)
- all_containers = self.project.containers(
- service_names=options['SERVICE'], stopped=True, one_off=one_off
- )
- 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),
- one_off=one_off
- )
- 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] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...]
- SERVICE [COMMAND] [ARGS...]
- Options:
- -d, --detach 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)
- -l, --label KEY=VAL Add or override a label (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.
- --use-aliases Use the service's network aliases in the network(s) the
- container connects to.
- -v, --volume=[] Bind mount a volume (default [])
- -T Disable pseudo-tty allocation. By default `docker-compose run`
- allocates a TTY.
- -w, --workdir="" Working directory inside the container
- """
- service = self.project.get_service(options['SERVICE'])
- detach = options.get('--detach')
- if options['--publish'] and options['--service-ports']:
- raise UserError(
- 'Service port mapping and manual port mapping '
- 'can not be used together'
- )
- if options['COMMAND'] is not None:
- command = [options['COMMAND']] + options['ARGS']
- elif options['--entrypoint'] is not None:
- command = []
- else:
- command = service.options.get('command')
- container_options = build_one_off_container_options(options, detach, command)
- run_one_off_container(
- container_options, self.project, service, options,
- self.toplevel_options, self.project_dir
- )
- 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
- This command is deprecated. Use the up command with the `--scale` flag
- instead.
- Usage: scale [options] [SERVICE=NUM...]
- Options:
- -t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
- (default: 10)
- """
- timeout = timeout_from_opts(options)
- if self.project.config_version == V2_2:
- raise UserError(
- 'The scale command is incompatible with the v2.2 format. '
- 'Use the up command with the --scale flag instead.'
- )
- else:
- log.warn(
- 'The scale command is deprecated. '
- 'Use the up command with the --scale flag instead.'
- )
- for service_name, num in parse_scale_args(options['SERVICE=NUM']).items():
- 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 = timeout_from_opts(options)
- 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 = timeout_from_opts(options)
- containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
- exit_if(not containers, 'No containers to restart', 1)
- def top(self, options):
- """
- Display the running processes
- Usage: top [SERVICE...]
- """
- containers = sorted(
- self.project.containers(service_names=options['SERVICE'], stopped=False) +
- self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
- key=attrgetter('name')
- )
- for idx, container in enumerate(containers):
- if idx > 0:
- print()
- top_data = self.project.client.top(container.name)
- headers = top_data.get("Titles")
- rows = []
- for process in top_data.get("Processes", []):
- rows.append(process)
- print(container.name)
- print(Formatter().table(headers, rows))
- 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] [--scale SERVICE=NUM...] [SERVICE...]
- Options:
- -d, --detach Detached mode: Run containers in the background,
- print new container names. Incompatible with
- --abort-on-container-exit.
- --no-color Produce monochrome output.
- --quiet-pull Pull without printing progress information
- --no-deps Don't start linked services.
- --force-recreate Recreate containers even if their configuration
- and image haven't changed.
- --always-recreate-deps Recreate dependent containers.
- Incompatible with --no-recreate.
- --no-recreate If containers already exist, don't recreate
- them. Incompatible with --force-recreate and -V.
- --no-build Don't build an image, even if it's missing.
- --no-start Don't start the services after creating them.
- --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)
- -V, --renew-anon-volumes Recreate anonymous volumes instead of retrieving
- data from the previous containers.
- --remove-orphans Remove containers for services not defined
- in the Compose file.
- --exit-code-from SERVICE Return the exit code of the selected service
- container. Implies --abort-on-container-exit.
- --scale SERVICE=NUM Scale SERVICE to NUM instances. Overrides the
- `scale` setting in the Compose file if present.
- """
- start_deps = not options['--no-deps']
- always_recreate_deps = options['--always-recreate-deps']
- exit_value_from = exitval_from_opts(options, self.project)
- cascade_stop = options['--abort-on-container-exit']
- service_names = options['SERVICE']
- timeout = timeout_from_opts(options)
- remove_orphans = options['--remove-orphans']
- detached = options.get('--detach')
- no_start = options.get('--no-start')
- if detached and (cascade_stop or exit_value_from):
- raise UserError("--abort-on-container-exit and -d cannot be combined.")
- environment = Environment.from_env_file(self.project_dir)
- ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS')
- if ignore_orphans and remove_orphans:
- raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.")
- opts = ['--detach', '--abort-on-container-exit', '--exit-code-from']
- for excluded in [x for x in opts if options.get(x) and no_start]:
- raise UserError('--no-start and {} cannot be combined.'.format(excluded))
- with up_shutdown_context(self.project, service_names, timeout, detached):
- warn_for_swarm_mode(self.project.client)
- def up(rebuild):
- return self.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,
- remove_orphans=remove_orphans,
- ignore_orphans=ignore_orphans,
- scale_override=parse_scale_args(options['--scale']),
- start=not no_start,
- always_recreate_deps=always_recreate_deps,
- reset_container_image=rebuild,
- renew_anonymous_volumes=options.get('--renew-anon-volumes'),
- silent=options.get('--quiet-pull'),
- )
- try:
- to_attach = up(False)
- except docker.errors.ImageNotFound as e:
- log.error(
- "The image for the service you're trying to recreate has been removed. "
- "If you continue, volume data could be lost. Consider backing up your data "
- "before continuing.\n".format(e.explanation)
- )
- res = yesno("Continue with the new image? [yN]", False)
- if res is None or not res:
- raise e
- to_attach = up(True)
- if detached or no_start:
- return
- attached_containers = filter_containers_to_service_names(to_attach, service_names)
- log_printer = log_printer_from_project(
- self.project,
- attached_containers,
- options['--no-color'],
- {'follow': True},
- cascade_stop,
- event_stream=self.project.events(service_names=service_names))
- print("Attaching to", list_containers(log_printer.containers))
- cascade_starter = log_printer.run()
- if cascade_stop:
- print("Aborting on container exit...")
- all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
- exit_code = compute_exit_code(
- exit_value_from, attached_containers, cascade_starter, all_containers
- )
- self.project.stop(service_names=service_names, timeout=timeout)
- if exit_value_from:
- exit_code = compute_service_exit_code(exit_value_from, attached_containers)
- sys.exit(exit_code)
- @classmethod
- def version(cls, options):
- """
- Show version information
- Usage: version [--short]
- Options:
- --short Shows only Compose's version number.
- """
- if options['--short']:
- print(__version__)
- else:
- print(get_version_info('full'))
- def compute_service_exit_code(exit_value_from, attached_containers):
- candidates = list(filter(
- lambda c: c.service == exit_value_from,
- attached_containers))
- if not candidates:
- log.error(
- 'No containers matching the spec "{0}" '
- 'were run.'.format(exit_value_from)
- )
- return 2
- if len(candidates) > 1:
- exit_values = filter(
- lambda e: e != 0,
- [c.inspect()['State']['ExitCode'] for c in candidates]
- )
- return exit_values[0]
- return candidates[0].inspect()['State']['ExitCode']
- def compute_exit_code(exit_value_from, attached_containers, cascade_starter, all_containers):
- exit_code = 0
- for e in all_containers:
- if (not e.is_running and cascade_starter == e.name):
- if not e.exit_code == 0:
- exit_code = e.exit_code
- break
- return exit_code
- def convergence_strategy_from_opts(options):
- no_recreate = options['--no-recreate']
- force_recreate = options['--force-recreate']
- renew_anonymous_volumes = options.get('--renew-anon-volumes')
- if force_recreate and no_recreate:
- raise UserError("--force-recreate and --no-recreate cannot be combined.")
- if no_recreate and renew_anonymous_volumes:
- raise UserError('--no-recreate and --renew-anon-volumes cannot be combined.')
- if force_recreate or renew_anonymous_volumes:
- return ConvergenceStrategy.always
- if no_recreate:
- return ConvergenceStrategy.never
- return ConvergenceStrategy.changed
- def timeout_from_opts(options):
- timeout = options.get('--timeout')
- return None if timeout is None else int(timeout)
- def image_digests_for_project(project, allow_push=False):
- try:
- return get_image_digests(
- project,
- allow_push=allow_push
- )
- except MissingDigests as e:
- def list_images(images):
- return "\n".join(" {}".format(name) for name in sorted(images))
- paras = ["Some images are missing digests."]
- if e.needs_push:
- command_hint = (
- "Use `docker-compose push {}` to push them. "
- .format(" ".join(sorted(e.needs_push)))
- )
- paras += [
- "The following images can be pushed:",
- list_images(e.needs_push),
- command_hint,
- ]
- if e.needs_pull:
- command_hint = (
- "Use `docker-compose pull {}` to pull them. "
- .format(" ".join(sorted(e.needs_pull)))
- )
- paras += [
- "The following images need to be pulled:",
- list_images(e.needs_pull),
- command_hint,
- ]
- raise UserError("\n\n".join(paras))
- def exitval_from_opts(options, project):
- exit_value_from = options.get('--exit-code-from')
- if exit_value_from:
- if not options.get('--abort-on-container-exit'):
- log.warn('using --exit-code-from implies --abort-on-container-exit')
- options['--abort-on-container-exit'] = True
- if exit_value_from not in [s.name for s in project.get_services()]:
- log.error('No service named "%s" was found in your compose file.',
- exit_value_from)
- sys.exit(2)
- return exit_value_from
- 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 build_one_off_container_options(options, detach, 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'] = Environment.from_command_line(
- parse_environment(options['-e'])
- )
- if options['--label']:
- container_options['labels'] = parse_labels(options['--label'])
- if options.get('--entrypoint') is not None:
- container_options['entrypoint'] = (
- [""] if options['--entrypoint'] == '' else options['--entrypoint']
- )
- # Ensure that run command remains one-off (issue #6302)
- 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['--name']:
- container_options['name'] = options['--name']
- if options['--workdir']:
- container_options['working_dir'] = options['--workdir']
- if options['--volume']:
- volumes = [VolumeSpec.parse(i) for i in options['--volume']]
- container_options['volumes'] = volumes
- return container_options
- def run_one_off_container(container_options, project, service, options, toplevel_options,
- project_dir='.'):
- if not options['--no-deps']:
- deps = service.get_dependency_names()
- if deps:
- project.up(
- service_names=deps,
- start_deps=True,
- strategy=ConvergenceStrategy.never,
- rescale=False
- )
- project.initialize()
- container = service.create_container(
- quiet=True,
- one_off=True,
- **container_options)
- use_network_aliases = options['--use-aliases']
- if options.get('--detach'):
- service.start_container(container, use_network_aliases)
- print(container.name)
- return
- def remove_container(force=False):
- if options['--rm']:
- project.client.remove_container(container.id, force=True, v=True)
- environment = Environment.from_env_file(project_dir)
- use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
- signals.set_signal_handler_to_shutdown()
- signals.set_signal_handler_to_hang_up()
- try:
- try:
- if IS_WINDOWS_PLATFORM or use_cli:
- service.connect_container_to_networks(container, use_network_aliases)
- exit_code = call_docker(
- ["start", "--attach", "--interactive", container.id],
- toplevel_options
- )
- else:
- operation = RunOperation(
- project.client,
- container.id,
- interactive=not options['-T'],
- logs=False,
- )
- pty = PseudoTerminal(project.client, operation)
- sockets = pty.sockets()
- service.start_container(container, use_network_aliases)
- pty.start(sockets)
- exit_code = container.wait()
- except (signals.ShutdownException):
- project.client.stop(container.id)
- exit_code = 1
- except (signals.ShutdownException, signals.HangUpException):
- 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)
- def call_docker(args, dockeropts):
- executable_path = find_executable('docker')
- if not executable_path:
- raise UserError(errors.docker_not_found_msg("Couldn't find `docker` binary."))
- tls = dockeropts.get('--tls', False)
- ca_cert = dockeropts.get('--tlscacert')
- cert = dockeropts.get('--tlscert')
- key = dockeropts.get('--tlskey')
- verify = dockeropts.get('--tlsverify')
- host = dockeropts.get('--host')
- tls_options = []
- if tls:
- tls_options.append('--tls')
- if ca_cert:
- tls_options.extend(['--tlscacert', ca_cert])
- if cert:
- tls_options.extend(['--tlscert', cert])
- if key:
- tls_options.extend(['--tlskey', key])
- if verify:
- tls_options.append('--tlsverify')
- if host:
- tls_options.extend(
- ['--host', re.sub(r'^https?://', 'tcp://', host.lstrip('='))]
- )
- args = [executable_path] + tls_options + args
- log.debug(" ".join(map(pipes.quote, args)))
- return subprocess.call(args)
- def parse_scale_args(options):
- res = {}
- for s in options:
- 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
- )
- res[service_name] = num
- return res
- def build_exec_command(options, container_id, command):
- args = ["exec"]
- if options["--detach"]:
- args += ["--detach"]
- else:
- args += ["--interactive"]
- if not options["-T"]:
- args += ["--tty"]
- if options["--privileged"]:
- args += ["--privileged"]
- if options["--user"]:
- args += ["--user", options["--user"]]
- if options["--env"]:
- for env_variable in options["--env"]:
- args += ["--env", env_variable]
- if options["--workdir"]:
- args += ["--workdir", options["--workdir"]]
- args += [container_id]
- args += command
- return args
- def has_container_with_state(containers, state):
- states = {
- 'running': lambda c: c.is_running,
- 'stopped': lambda c: not c.is_running,
- 'paused': lambda c: c.is_paused,
- 'restarting': lambda c: c.is_restarting,
- }
- for container in containers:
- if state not in states:
- raise UserError("Invalid state: %s" % state)
- if states[state](container):
- return True
- def filter_services(filt, services, project):
- def should_include(service):
- for f in filt:
- if f == 'status':
- state = filt[f]
- containers = project.containers([service.name], stopped=True)
- if not has_container_with_state(containers, state):
- return False
- elif f == 'source':
- source = filt[f]
- if source == 'image' or source == 'build':
- if source not in service.options:
- return False
- else:
- raise UserError("Invalid value for source filter: %s" % source)
- else:
- raise UserError("Invalid filter: %s" % f)
- return True
- return filter(should_include, services)
- def build_filter(arg):
- filt = {}
- if arg is not None:
- if '=' not in arg:
- raise UserError("Arguments to --filter should be in form KEY=VAL")
- key, val = arg.split('=', 1)
- filt[key] = val
- return filt
- def warn_for_swarm_mode(client):
- info = client.info()
- if info.get('Swarm', {}).get('LocalNodeState') == 'active':
- if info.get('ServerVersion', '').startswith('ucp'):
- # UCP does multi-node scheduling with traditional Compose files.
- return
- log.warn(
- "The Docker Engine you're using is running in swarm mode.\n\n"
- "Compose does not use swarm mode to deploy services to multiple nodes in a swarm. "
- "All containers will be scheduled on the current node.\n\n"
- "To deploy your application across the swarm, "
- "use `docker stack deploy`.\n"
- )
|