From bf64d9a0f50cc8b5ff4a3dbd232e60b26426b553 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Fri, 18 Apr 2025 14:30:52 +0100 Subject: [PATCH] Run pre-connect hooks before ssh commands We hook into the SSHKit `on` method to run the pre-connect hook before the first SSH command. This doesn't work for interactive exec commands where ssh is called directly. Fixes: https://github.com/basecamp/kamal/issues/1157 --- lib/kamal/cli/accessory.rb | 2 ++ lib/kamal/cli/app.rb | 2 ++ lib/kamal/cli/base.rb | 8 ++++++-- lib/kamal/cli/server.rb | 2 ++ test/cli/app_test.rb | 6 ++++++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/kamal/cli/accessory.rb b/lib/kamal/cli/accessory.rb index 735d554e..e906b3b3 100644 --- a/lib/kamal/cli/accessory.rb +++ b/lib/kamal/cli/accessory.rb @@ -141,6 +141,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base 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" def exec(name, *cmd) + pre_connect_if_required + cmd = Kamal::Utils.join_commands(cmd) with_accessory(name) do |accessory, hosts| case diff --git a/lib/kamal/cli/app.rb b/lib/kamal/cli/app.rb index 54d8a164..a449efdf 100644 --- a/lib/kamal/cli/app.rb +++ b/lib/kamal/cli/app.rb @@ -106,6 +106,8 @@ class Kamal::Cli::App < Kamal::Cli::Base option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" option :detach, type: :boolean, default: false, desc: "Execute command in a detached container" def exec(*cmd) + pre_connect_if_required + if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence) raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}" end diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 0a6dd6e7..45b1411e 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -147,12 +147,16 @@ module Kamal::Cli end def on(*args, &block) + pre_connect_if_required + + super + end + + def pre_connect_if_required if !KAMAL.connected? run_hook "pre-connect" KAMAL.connected = true end - - super end def command diff --git a/lib/kamal/cli/server.rb b/lib/kamal/cli/server.rb index db464e7b..e3da8c18 100644 --- a/lib/kamal/cli/server.rb +++ b/lib/kamal/cli/server.rb @@ -2,6 +2,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) + pre_connect_if_required + cmd = Kamal::Utils.join_commands(cmd) hosts = KAMAL.hosts diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 2ed00182..c39176c3 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -334,18 +334,24 @@ class CliAppTest < CliTestCase end test "exec interactive" 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 run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'") + run_command("exec", "-i", "ruby -v").tap do |output| + assert_hook_ran "pre-connect", output assert_match "Get most recent version available as an image...", output assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output end end test "exec interactive with reuse" 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 -it app-web-999 ruby -v'") + run_command("exec", "-i", "--reuse", "ruby -v").tap do |output| + assert_hook_ran "pre-connect", output assert_match "Get current version of running container...", output assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done 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