Merge pull request #1544 from prullmann/kamal-exec-piping
Allow piping into kamal exec #1485
This commit is contained in:
@@ -56,14 +56,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
|
|
||||||
def execute_in_existing_container(*command, interactive: false)
|
def execute_in_existing_container(*command, interactive: false)
|
||||||
docker :exec,
|
docker :exec,
|
||||||
("-it" if interactive),
|
(docker_interactive_args if interactive),
|
||||||
service_name,
|
service_name,
|
||||||
*command
|
*command
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute_in_new_container(*command, interactive: false)
|
def execute_in_new_container(*command, interactive: false)
|
||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
(docker_interactive_args if interactive),
|
||||||
"--rm",
|
"--rm",
|
||||||
*network_args,
|
*network_args,
|
||||||
*env_args,
|
*env_args,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module Kamal::Commands::App::Execution
|
module Kamal::Commands::App::Execution
|
||||||
def execute_in_existing_container(*command, interactive: false, env:)
|
def execute_in_existing_container(*command, interactive: false, env:)
|
||||||
docker :exec,
|
docker :exec,
|
||||||
("-it" if interactive),
|
(docker_interactive_args if interactive),
|
||||||
*argumentize("--env", env),
|
*argumentize("--env", env),
|
||||||
container_name,
|
container_name,
|
||||||
*command
|
*command
|
||||||
@@ -9,7 +9,7 @@ module Kamal::Commands::App::Execution
|
|||||||
|
|
||||||
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
(docker_interactive_args if interactive),
|
||||||
("--detach" if detach),
|
("--detach" if detach),
|
||||||
("--rm" unless detach),
|
("--rm" unless detach),
|
||||||
"--network", "kamal",
|
"--network", "kamal",
|
||||||
|
|||||||
@@ -122,5 +122,9 @@ module Kamal::Commands
|
|||||||
def ensure_local_buildx_installed
|
def ensure_local_buildx_installed
|
||||||
docker :buildx, "version"
|
docker :buildx, "version"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def docker_interactive_args
|
||||||
|
STDIN.isatty ? "-it" : "-i"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ class CliAppTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'")
|
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'")
|
||||||
|
|
||||||
|
stub_stdin_tty do
|
||||||
run_command("exec", "-i", "ruby -v").tap do |output|
|
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||||
assert_hook_ran "pre-connect", output
|
assert_hook_ran "pre-connect", output
|
||||||
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
||||||
@@ -368,12 +369,14 @@ class CliAppTest < CliTestCase
|
|||||||
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "exec interactive with reuse" do
|
test "exec interactive with reuse" do
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
||||||
|
|
||||||
|
stub_stdin_tty do
|
||||||
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||||
assert_hook_ran "pre-connect", output
|
assert_hook_ran "pre-connect", output
|
||||||
assert_match "Get current version of running container...", output
|
assert_match "Get current version of running container...", output
|
||||||
@@ -381,6 +384,20 @@ class CliAppTest < CliTestCase
|
|||||||
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec interactive with pipe on STDIN" do
|
||||||
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -i app-web-999 ruby -v'")
|
||||||
|
|
||||||
|
stub_stdin_file do
|
||||||
|
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||||
|
assert_hook_ran "pre-connect", output
|
||||||
|
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "containers" do
|
||||||
run_command("containers").tap do |output|
|
run_command("containers").tap do |output|
|
||||||
|
|||||||
@@ -118,14 +118,21 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
test "execute in new container over ssh" do
|
test "execute in new container over ssh" do
|
||||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root},
|
assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root},
|
||||||
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
stub_stdin_tty { new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in existing container over ssh" do
|
test "execute in existing container over ssh" do
|
||||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
assert_match %r{docker exec -it app-mysql mysql -u root},
|
assert_match %r{docker exec -it app-mysql mysql -u root},
|
||||||
new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root")
|
stub_stdin_tty { new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root") }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in existing container with piped input over ssh" do
|
||||||
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
|
assert_match %r{docker exec -i app-mysql mysql -u root},
|
||||||
|
stub_stdin_file { new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root") }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "execute in new container over ssh" do
|
test "execute in new container over ssh" do
|
||||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c},
|
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in new container over ssh with tags" do
|
test "execute in new container over ssh with tags" do
|
||||||
@@ -296,18 +296,23 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails c'",
|
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails c'",
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in new container with custom options over ssh" do
|
test "execute in new container with custom options over ssh" do
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in existing container over ssh" do
|
test "execute in existing container over ssh" do
|
||||||
assert_match %r{docker exec -it app-web-999 bin/rails c},
|
assert_match %r{docker exec -it app-web-999 bin/rails c},
|
||||||
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {})
|
stub_stdin_tty { new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {}) }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in existing container with piped input over ssh" do
|
||||||
|
assert_match %r{docker exec -i app-web-999 bin/rails c},
|
||||||
|
stub_stdin_file { new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {}) }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run over ssh" do
|
test "run over ssh" do
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ require "active_support/test_case"
|
|||||||
require "active_support/testing/autorun"
|
require "active_support/testing/autorun"
|
||||||
require "active_support/testing/stream"
|
require "active_support/testing/stream"
|
||||||
require "rails/test_unit/line_filtering"
|
require "rails/test_unit/line_filtering"
|
||||||
|
require "pty"
|
||||||
require "debug"
|
require "debug"
|
||||||
require "mocha/minitest" # using #stubs that can alter returns
|
require "mocha/minitest" # using #stubs that can alter returns
|
||||||
require "minitest/autorun" # using #stub that take args
|
require "minitest/autorun" # using #stub that take args
|
||||||
@@ -48,6 +49,27 @@ class ActiveSupport::TestCase
|
|||||||
capture(:stderr) { yield }.strip
|
capture(:stderr) { yield }.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stub_stdin_tty
|
||||||
|
PTY.open do |master, slave|
|
||||||
|
stub_stdin(master) { yield }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_stdin_file
|
||||||
|
File.open("/dev/null", "r") do |file|
|
||||||
|
stub_stdin(file) { yield }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_stdin(io)
|
||||||
|
original_stdin = STDIN.dup
|
||||||
|
STDIN.reopen(io)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
STDIN.reopen(original_stdin)
|
||||||
|
original_stdin.close
|
||||||
|
end
|
||||||
|
|
||||||
def with_test_secrets(**files)
|
def with_test_secrets(**files)
|
||||||
setup_test_secrets(**files)
|
setup_test_secrets(**files)
|
||||||
yield
|
yield
|
||||||
|
|||||||
Reference in New Issue
Block a user