Allow multiple arguments for exec commands

If you can have an alias like:

```
aliases:
  rails: app exec -p rails
```

Then `kamal rails db:migrate:status` will execute
`kamal app exec -p rails db:migrate:status`.

So this works, we'll allow multiple arguments `app exec` and
`server exec` to accept multiple arguments.

The arguments are combined by simply joining them with a space. This
means that these are equivalent:

```
kamal app exec -p rails db:migrate:status
kamal app exec -p "rails db:migrate:status"
```

If you want to pass an argument with spaces, you'll need to quote it:

```
kamal app exec -p "git commit -am \"My comment\""
kamal app exec -p git commit -am "\"My comment\""
```
This commit is contained in:
Donal McBreen
2024-08-28 10:58:25 +01:00
parent b8af719bb7
commit 579e169be2
9 changed files with 43 additions and 5 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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