diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 19bf84cd..ce21b2cf 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -71,11 +71,12 @@ class Kamal::Cli::App < Kamal::Cli::Base end end - desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)" + desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)" option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one" option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" - def exec(cmd) + def exec(*cmd) + cmd = Kamal::Utils.join_commands(cmd) env = options[:env] case when options[:interactive] && options[:reuse] diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index b545050f..5b1b0cc7 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -1,7 +1,8 @@ class Kamal::Cli::Server < Kamal::Cli::Base desc "exec", "Run a custom command on the server (use --help to show options)" option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)" - def exec(cmd) + def exec(*cmd) + cmd = Kamal::Utils.join_commands(cmd) hosts = KAMAL.hosts | KAMAL.accessory_hosts case diff --git a/lib/kamal/utils.rb b/lib/kamal/utils.rb index e6b28e43..a6ba5702 100644 --- a/lib/kamal/utils.rb +++ b/lib/kamal/utils.rb @@ -77,4 +77,8 @@ module Kamal::Utils def stable_sort!(elements, &block) elements.sort_by!.with_index { |element, index| [ block.call(element), index ] } end + + def join_commands(commands) + commands.map(&:strip).join(" ") + end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 27e3ed9d..8218dba8 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -247,6 +247,12 @@ class CliAppTest < CliTestCase end end + test "exec separate arguments" do + run_command("exec", "ruby", " -v").tap do |output| + assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output + end + end + test "exec with reuse" do run_command("exec", "--reuse", "ruby -v").tap do |output| assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index fa0ed553..60192cf6 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -566,6 +566,13 @@ class CliMainTest < CliTestCase end end + test "append to command with an alias" do + run_command("rails", "db:migrate:status", config_file: "deploy_with_aliases").tap do |output| + assert_match "docker exec app-console-999 rails db:migrate:status on 1.1.1.5", output + assert_match "App Host: 1.1.1.5", output + end + end + private def run_command(*command, config_file: "deploy_simple") with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index 5d9fec4d..110e217d 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -3,8 +3,8 @@ require_relative "cli_test_case" class CliServerTest < CliTestCase test "running a command with exec" do SSHKit::Backend::Abstract.any_instance.stubs(:capture) - .with("date", verbosity: 1) - .returns("Today") + .with("date", verbosity: 1) + .returns("Today") hosts = "1.1.1.1".."1.1.1.4" run_command("exec", "date").tap do |output| @@ -15,6 +15,20 @@ class CliServerTest < CliTestCase end end + test "running a command with exec multiple arguments" do + SSHKit::Backend::Abstract.any_instance.stubs(:capture) + .with("date -j", verbosity: 1) + .returns("Today") + + hosts = "1.1.1.1".."1.1.1.4" + run_command("exec", "date", "-j").tap do |output| + hosts.map do |host| + assert_match "Running 'date -j' on #{hosts.to_a.join(', ')}...", output + assert_match "App Host: #{host}\nToday", output + end + end + end + test "bootstrap already installed" do stub_setup SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once diff --git a/test/fixtures/deploy_with_aliases.yml b/test/fixtures/deploy_with_aliases.yml index 6a8f00b8..bb54fa76 100644 --- a/test/fixtures/deploy_with_aliases.yml +++ b/test/fixtures/deploy_with_aliases.yml @@ -18,3 +18,4 @@ aliases: info: details console: app exec --reuse -p -r console "bin/console" exec: app exec --reuse -p -r console + rails: app exec --reuse -p -r console rails diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy.yml b/test/integration/docker/deployer/app_with_roles/config/deploy.yml index e5c6e28a..7133c023 100644 --- a/test/integration/docker/deployer/app_with_roles/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_roles/config/deploy.yml @@ -40,3 +40,4 @@ readiness_delay: 0 aliases: whome: version worker_hostname: app exec -r workers -q --reuse hostname + uname: server exec -q -p uname diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 8ea04b11..742e111c 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -93,6 +93,9 @@ class MainTest < IntegrationTest output = kamal :worker_hostname, capture: true assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output + + output = kamal :uname, "-o", capture: true + assert_match "App Host: vm1\nGNU/Linux", output end test "setup and remove" do