From 2fb5c9ac912309a45c9d9f61b13506c6c3810a75 Mon Sep 17 00:00:00 2001 From: Onno Steenbergen Date: Wed, 25 Mar 2015 22:42:04 +0100 Subject: [PATCH 1/2] Added execute command. Example: $ docker-compose execute web echo "Hello World!" Supports --detach for background running Supports running command on all containers with: $ docker-compose execute all echo "Hello World!" Signed-off-by: Onno Steenbergen --- compose/cli/main.py | 43 +++++++++++++++ compose/container.py | 3 ++ tests/fixtures/long_running/Dockerfile | 2 + .../fixtures/long_running/docker-compose.yml | 5 ++ tests/integration/cli_test.py | 53 +++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 tests/fixtures/long_running/Dockerfile create mode 100644 tests/fixtures/long_running/docker-compose.yml diff --git a/compose/cli/main.py b/compose/cli/main.py index 95dfb6cbd36..6c3b131944a 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -90,6 +90,7 @@ class TopLevelCommand(Command): pull Pulls service images rm Remove stopped containers run Run a one-off command + execute Run a command in a service scale Set number of containers for a service start Start services stop Stop services @@ -349,6 +350,48 @@ def run(self, project, options): project.client.remove_container(container.id) sys.exit(exit_code) + def execute(self, project, options): + """ + Run a command in a running service + + For example: + $ docker-compose execute web ps aux + + To run against all services use "all" instead of a service: + $ docker-compose execute all ps aux + + Usage: execute [options] SERVICE CMD... + + Options: + -d, --detach Detached mode: run command in the background. + (default: false) + """ + service = options['SERVICE'] + + # Should we run against all containers? + if service in ['all']: + containers = sorted( + project.containers() + + project.containers(one_off=True), + key=attrgetter('name')) + else: + containers = sorted( + project.containers(service_names=[options['SERVICE']]) + + project.containers(service_names=[options['SERVICE']], one_off=True), + key=attrgetter('name')) + + detach = options.get('--detach') + # Run the command against all containers + for container in containers: + result = container.execute(options['CMD'], detach=detach) + + # only show container name in detach mode + if detach: + print(container.name) + else: + print("----- %s -----\n" % container.name) + print(result) + def scale(self, project, options): """ Set number of containers to run for a service. diff --git a/compose/container.py b/compose/container.py index 1d044a421ce..4170df79606 100644 --- a/compose/container.py +++ b/compose/container.py @@ -156,6 +156,9 @@ def links(self): links.append(bits[2]) return links + def execute(self, *args, **kwargs): + return self.client.execute(self.id, *args, **kwargs) + def attach(self, *args, **kwargs): return self.client.attach(self.id, *args, **kwargs) diff --git a/tests/fixtures/long_running/Dockerfile b/tests/fixtures/long_running/Dockerfile new file mode 100644 index 00000000000..79b85e0f155 --- /dev/null +++ b/tests/fixtures/long_running/Dockerfile @@ -0,0 +1,2 @@ +FROM busybox:latest +CMD sleep 300 diff --git a/tests/fixtures/long_running/docker-compose.yml b/tests/fixtures/long_running/docker-compose.yml new file mode 100644 index 00000000000..f183d7fdc62 --- /dev/null +++ b/tests/fixtures/long_running/docker-compose.yml @@ -0,0 +1,5 @@ +longrunning: + build: tests/fixtures/long_running + +longrunning2: + build: tests/fixtures/long_running diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index df3eec66d08..91ac2abefdc 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -318,6 +318,59 @@ def test_run_service_with_map_ports(self, __): self.assertIn("0.0.0.0", port_random) self.assertEqual(port_assigned, "0.0.0.0:49152") + + @patch('sys.stdout', new_callable=StringIO) + def test_execute_one(self, mock_stdout): + """ + Execute a command in a single container for a single service + """ + self.command.base_dir = 'tests/fixtures/long_running' + self.command.dispatch(['up', '-d', 'longrunning'], None) + + # Validate the start + service = self.project.get_service('longrunning') + self.assertEqual(len(service.containers()), 1) + + self.command.dispatch(['execute','longrunning','echo','Hello World!'], None) + output = mock_stdout.getvalue() + self.assertIn('Hello World!',output) + + @patch('sys.stdout', new_callable=StringIO) + def test_execute_multiple(self, mock_stdout): + """ + Execute a command in a single container for a single service + """ + self.command.base_dir = 'tests/fixtures/long_running' + self.command.dispatch(['up', '-d', 'longrunning'], None) + self.command.dispatch(['scale', 'longrunning=2'], None) + + # Validate the start/scaling + service = self.project.get_service('longrunning') + self.assertEqual(len(service.containers()), 2) + + self.command.dispatch(['execute','longrunning','echo','Hello World!'], None) + output = mock_stdout.getvalue() + self.assertEqual(output.count('Hello World!'), 2) + + @patch('sys.stdout', new_callable=StringIO) + def test_execute_all(self, mock_stdout): + """ + Execute a command in a multiple containers for a multple service + """ + self.command.base_dir = 'tests/fixtures/long_running' + self.command.dispatch(['up', '-d'], None) + self.command.dispatch(['scale', 'longrunning=2', 'longrunning2=3'], None) + + # Validate the start/scaling + service = self.project.get_service('longrunning') + self.assertEqual(len(service.containers()), 2) + service2 = self.project.get_service('longrunning2') + self.assertEqual(len(service2.containers()), 3) + + self.command.dispatch(['execute','all','echo','Hello World!'], None) + output = mock_stdout.getvalue() + self.assertEqual(output.count('Hello World!'), 5) + def test_rm(self): service = self.project.get_service('simple') service.create_container() From 2ad8f83fd7170de187b02f346b07c2621ba90bb4 Mon Sep 17 00:00:00 2001 From: Onno Steenbergen Date: Wed, 25 Mar 2015 22:42:04 +0100 Subject: [PATCH 2/2] Execute command now named exec all is now an options (--all) Renamed long_running to long-running so all folders have the same name convention exec is a python reserved word so needed to change docopt_command to allow for reserved words. Signed-off-by: Onno Steenbergen --- compose/cli/docopt_command.py | 6 +++- compose/cli/main.py | 28 ++++++++++++------- .../{long_running => long-running}/Dockerfile | 0 .../fixtures/long-running/docker-compose.yml | 5 ++++ .../fixtures/long_running/docker-compose.yml | 5 ---- tests/integration/cli_test.py | 18 ++++++------ 6 files changed, 37 insertions(+), 25 deletions(-) rename tests/fixtures/{long_running => long-running}/Dockerfile (100%) create mode 100644 tests/fixtures/long-running/docker-compose.yml delete mode 100644 tests/fixtures/long_running/docker-compose.yml diff --git a/compose/cli/docopt_command.py b/compose/cli/docopt_command.py index 8105d3b3feb..be37afe99d7 100644 --- a/compose/cli/docopt_command.py +++ b/compose/cli/docopt_command.py @@ -34,7 +34,11 @@ def parse(self, argv, global_options): raise SystemExit(getdoc(self)) if not hasattr(self, command): - raise NoSuchCommand(command, self) + # A command can be a reserved word, like exec + if hasattr(self, "%s_" % command): + command = "%s_" % command + else: + raise NoSuchCommand(command, self) handler = getattr(self, command) docstring = getdoc(handler) diff --git a/compose/cli/main.py b/compose/cli/main.py index 6c3b131944a..bfd69e80304 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -90,7 +90,7 @@ class TopLevelCommand(Command): pull Pulls service images rm Remove stopped containers run Run a one-off command - execute Run a command in a service + exec Run a command in a service scale Set number of containers for a service start Start services stop Stop services @@ -350,40 +350,48 @@ def run(self, project, options): project.client.remove_container(container.id) sys.exit(exit_code) - def execute(self, project, options): + # exec is Python reserved word, redirected in docopt_command.py + def exec_(self, project, options): """ Run a command in a running service For example: - $ docker-compose execute web ps aux + $ docker-compose exec web ps aux - To run against all services use "all" instead of a service: - $ docker-compose execute all ps aux + To run against all services: + $ docker-compose exec --all ps aux - Usage: execute [options] SERVICE CMD... + Usage: exec [options] [ SERVICE CMD... | CMD... ] Options: + -a, --all Run against all containers of all services -d, --detach Detached mode: run command in the background. (default: false) """ service = options['SERVICE'] + cmd = options['CMD'] # Should we run against all containers? - if service in ['all']: + if options['--all']: containers = sorted( project.containers() + project.containers(one_off=True), key=attrgetter('name')) + + # If we have a service it should be in the command + if service is not None: + cmd.insert(0, service) else: containers = sorted( - project.containers(service_names=[options['SERVICE']]) + - project.containers(service_names=[options['SERVICE']], one_off=True), + project.containers(service_names=[service]) + + project.containers(service_names=[service], one_off=True), key=attrgetter('name')) detach = options.get('--detach') + # Run the command against all containers for container in containers: - result = container.execute(options['CMD'], detach=detach) + result = container.execute(cmd, detach=detach) # only show container name in detach mode if detach: diff --git a/tests/fixtures/long_running/Dockerfile b/tests/fixtures/long-running/Dockerfile similarity index 100% rename from tests/fixtures/long_running/Dockerfile rename to tests/fixtures/long-running/Dockerfile diff --git a/tests/fixtures/long-running/docker-compose.yml b/tests/fixtures/long-running/docker-compose.yml new file mode 100644 index 00000000000..c8f1e9204b8 --- /dev/null +++ b/tests/fixtures/long-running/docker-compose.yml @@ -0,0 +1,5 @@ +longrunning: + build: tests/fixtures/long-running + +longrunning2: + build: tests/fixtures/long-running diff --git a/tests/fixtures/long_running/docker-compose.yml b/tests/fixtures/long_running/docker-compose.yml deleted file mode 100644 index f183d7fdc62..00000000000 --- a/tests/fixtures/long_running/docker-compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -longrunning: - build: tests/fixtures/long_running - -longrunning2: - build: tests/fixtures/long_running diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 91ac2abefdc..5f59f47490a 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -320,27 +320,27 @@ def test_run_service_with_map_ports(self, __): @patch('sys.stdout', new_callable=StringIO) - def test_execute_one(self, mock_stdout): + def test_exec_one(self, mock_stdout): """ Execute a command in a single container for a single service """ - self.command.base_dir = 'tests/fixtures/long_running' + self.command.base_dir = 'tests/fixtures/long-running' self.command.dispatch(['up', '-d', 'longrunning'], None) # Validate the start service = self.project.get_service('longrunning') self.assertEqual(len(service.containers()), 1) - self.command.dispatch(['execute','longrunning','echo','Hello World!'], None) + self.command.dispatch(['exec','longrunning','echo','Hello World!'], None) output = mock_stdout.getvalue() self.assertIn('Hello World!',output) @patch('sys.stdout', new_callable=StringIO) - def test_execute_multiple(self, mock_stdout): + def test_exec_multiple(self, mock_stdout): """ Execute a command in a single container for a single service """ - self.command.base_dir = 'tests/fixtures/long_running' + self.command.base_dir = 'tests/fixtures/long-running' self.command.dispatch(['up', '-d', 'longrunning'], None) self.command.dispatch(['scale', 'longrunning=2'], None) @@ -348,16 +348,16 @@ def test_execute_multiple(self, mock_stdout): service = self.project.get_service('longrunning') self.assertEqual(len(service.containers()), 2) - self.command.dispatch(['execute','longrunning','echo','Hello World!'], None) + self.command.dispatch(['exec','longrunning','echo','Hello World!'], None) output = mock_stdout.getvalue() self.assertEqual(output.count('Hello World!'), 2) @patch('sys.stdout', new_callable=StringIO) - def test_execute_all(self, mock_stdout): + def test_exec_all(self, mock_stdout): """ Execute a command in a multiple containers for a multple service """ - self.command.base_dir = 'tests/fixtures/long_running' + self.command.base_dir = 'tests/fixtures/long-running' self.command.dispatch(['up', '-d'], None) self.command.dispatch(['scale', 'longrunning=2', 'longrunning2=3'], None) @@ -367,7 +367,7 @@ def test_execute_all(self, mock_stdout): service2 = self.project.get_service('longrunning2') self.assertEqual(len(service2.containers()), 3) - self.command.dispatch(['execute','all','echo','Hello World!'], None) + self.command.dispatch(['exec','--all','echo','Hello World!'], None) output = mock_stdout.getvalue() self.assertEqual(output.count('Hello World!'), 5)