diff --git a/compose/cli/docopt_command.py b/compose/cli/docopt_command.py index e3f4aa9e5..d2900b392 100644 --- a/compose/cli/docopt_command.py +++ b/compose/cli/docopt_command.py @@ -43,6 +43,10 @@ class DocoptCommand(object): def get_handler(self, command): command = command.replace('-', '_') + # we certainly want to have "exec" command, since that's what docker client has + # but in python exec is a keyword + if command == "exec": + command = "exec_command" if not hasattr(self, command): raise NoSuchCommand(command, self) diff --git a/compose/cli/main.py b/compose/cli/main.py index 6a04f9f00..2c0fda1cf 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -43,7 +43,7 @@ from .utils import yesno if not IS_WINDOWS_PLATFORM: - from dockerpty.pty import PseudoTerminal, RunOperation + from dockerpty.pty import PseudoTerminal, RunOperation, ExecOperation log = logging.getLogger(__name__) console_handler = logging.StreamHandler(sys.stderr) @@ -152,6 +152,7 @@ class TopLevelCommand(DocoptCommand): 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 @@ -298,6 +299,57 @@ class TopLevelCommand(DocoptCommand): print(formatter(event)) sys.stdout.flush() + def exec_command(self, project, 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 = 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( + project.client, + exec_id, + interactive=tty, + ) + pty = PseudoTerminal(project.client, operation) + pty.start() + except signals.ShutdownException: + log.info("received shutdown exception: closing") + exit_code = project.client.exec_inspect(exec_id).get("ExitCode") + sys.exit(exit_code) + def help(self, project, options): """ Get help on a command. diff --git a/compose/container.py b/compose/container.py index c96b63ef4..6dac94999 100644 --- a/compose/container.py +++ b/compose/container.py @@ -216,6 +216,12 @@ class Container(object): def remove(self, **options): return self.client.remove_container(self.id, **options) + def create_exec(self, command, **options): + return self.client.exec_create(self.id, command, **options) + + def start_exec(self, exec_id, **options): + return self.client.exec_start(exec_id, **options) + def rename_to_tmp_name(self): """Rename the container to a hopefully unique temporary container name by prepending the short id. diff --git a/docs/reference/exec.md b/docs/reference/exec.md new file mode 100644 index 000000000..6c0eeb04d --- /dev/null +++ b/docs/reference/exec.md @@ -0,0 +1,29 @@ + + +# exec + +``` +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] +``` + +This is equivalent of `docker exec`. With this subcommand you can run arbitrary +commands in your services. Commands are by default allocating a TTY, so you can +do e.g. `docker-compose exec web sh` to get an interactive prompt. diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 02f828727..2b61898e7 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -752,6 +752,24 @@ class CLITestCase(DockerClientTestCase): self.project.stop(['simple']) wait_on_condition(ContainerCountCondition(self.project, 0)) + def test_exec_without_tty(self): + self.base_dir = 'tests/fixtures/links-composefile' + self.dispatch(['up', '-d', 'console']) + self.assertEqual(len(self.project.containers()), 1) + + stdout, stderr = self.dispatch(['exec', '-T', 'console', 'ls', '-1d', '/']) + self.assertEquals(stdout, "/\n") + self.assertEquals(stderr, "") + + def test_exec_custom_user(self): + self.base_dir = 'tests/fixtures/links-composefile' + self.dispatch(['up', '-d', 'console']) + self.assertEqual(len(self.project.containers()), 1) + + stdout, stderr = self.dispatch(['exec', '-T', '--user=operator', 'console', 'whoami']) + self.assertEquals(stdout, "operator\n") + self.assertEquals(stderr, "") + def test_run_service_without_links(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['run', 'console', '/bin/true'])