Avoid setting env via SSHKit

SSHKit puts the env in the command, so leaks them in process listings.
This commit is contained in:
Donal McBreen
2024-09-19 14:58:13 +01:00
parent 45a58f7e15
commit 2c829a4824
7 changed files with 63 additions and 74 deletions

View File

@@ -135,8 +135,10 @@ module Kamal::Cli
details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand } details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
say "Running the #{hook} hook...", :magenta say "Running the #{hook} hook...", :magenta
run_locally do with_env KAMAL.hook.env(**details, **extra_details) do
execute *KAMAL.hook.run(hook, **details, **extra_details) run_locally do
execute *KAMAL.hook.run(hook)
end
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}") raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
end end
@@ -183,5 +185,14 @@ module Kamal::Cli
execute(*KAMAL.server.ensure_run_directory) execute(*KAMAL.server.ensure_run_directory)
end end
end end
def with_env(env)
current_env = ENV.to_h.dup
ENV.update(env)
yield
ensure
ENV.clear
ENV.update(current_env)
end
end end
end end

View File

@@ -30,28 +30,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end end
run_locally do with_env(KAMAL.config.builder.secrets) do
begin run_locally do
execute *KAMAL.builder.inspect_builder begin
rescue SSHKit::Command::Failed => e execute *KAMAL.builder.inspect_builder
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/ rescue SSHKit::Command::Failed => e
warn "Missing compatible builder, so creating a new one first" if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
begin warn "Missing compatible builder, so creating a new one first"
cli.remove begin
rescue SSHKit::Command::Failed cli.remove
raise unless e.message =~ /(context not found|no builder|does not exist)/ rescue SSHKit::Command::Failed
raise unless e.message =~ /(context not found|no builder|does not exist)/
end
cli.create
else
raise
end end
cli.create
else
raise
end end
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it # Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push push = KAMAL.builder.push
KAMAL.with_verbosity(:debug) do KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets } Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end end
end end
end end

View File

@@ -48,7 +48,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login" desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login"
@@ -75,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end end
desc "rollback [VERSION]", "Rollback app to VERSION" desc "rollback [VERSION]", "Rollback app to VERSION"
@@ -99,7 +99,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end end
end end
run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
end end
desc "details", "Show details about all containers" desc "details", "Show details about all containers"

View File

@@ -1,9 +1,12 @@
class Kamal::Commands::Hook < Kamal::Commands::Base class Kamal::Commands::Hook < Kamal::Commands::Base
def run(hook, secrets: false, **details) def run(hook)
env = tags(**details).env [ hook_file(hook) ]
env.merge!(config.secrets.to_h) if secrets end
[ hook_file(hook), env: env ] def env(secrets: false, **details)
tags(**details).env.tap do |env|
env.merge!(config.secrets.to_h) if secrets
end
end end
def hook_exists?(hook) def hook_exists?(hook)

View File

@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD) .with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -140,7 +140,7 @@ class CliBuildTest < CliTestCase
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {}) .with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output| run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output assert_match /WARN Missing compatible builder, so creating a new one first/, output

View File

@@ -41,27 +41,7 @@ class CliTestCase < ActiveSupport::TestCase
end end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false) def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
whoami = `whoami`.chomp assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output
performer = Kamal::Git.email.presence || whoami
service = service_version.split("@").first
assert_match "Running the #{hook} hook...\n", output
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
KAMAL_PERFORMER=\"#{performer}\"\s
KAMAL_VERSION=\"#{version}\"\s
KAMAL_SERVICE_VERSION=\"#{service_version}\"\s
KAMAL_SERVICE=\"#{service}\"\s
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
#{"DB_PASSWORD=\"secret\"\\s" if secrets}
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
assert_match expected, output
end end
def with_argv(*argv) def with_argv(*argv)

View File

@@ -16,41 +16,34 @@ class CommandsHookTest < ActiveSupport::TestCase
end end
test "run" do test "run" do
assert_equal [ assert_equal [ ".kamal/hooks/foo" ], new_command.run("foo")
".kamal/hooks/foo", end
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at, test "env" do
"KAMAL_PERFORMER" => @performer, assert_equal ({
"KAMAL_VERSION" => "123", "KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_SERVICE_VERSION" => "app@123", "KAMAL_PERFORMER" => @performer,
"KAMAL_SERVICE" => "app" } } "KAMAL_VERSION" => "123",
], new_command.run("foo") "KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app"
}), new_command.env
end end
test "run with custom hooks_path" do test "run with custom hooks_path" do
assert_equal [ assert_equal [ "custom/hooks/path/foo" ], new_command(hooks_path: "custom/hooks/path").run("foo")
"custom/hooks/path/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command(hooks_path: "custom/hooks/path").run("foo")
end end
test "hook with secrets" do test "env with secrets" do
with_test_secrets("secrets" => "DB_PASSWORD=secret") do with_test_secrets("secrets" => "DB_PASSWORD=secret") do
assert_equal [ assert_equal (
".kamal/hooks/foo", {
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at, "KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer, "KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123", "KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123", "KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app", "KAMAL_SERVICE" => "app",
"DB_PASSWORD" => "secret" } } "DB_PASSWORD" => "secret" }
], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true) ), new_command.env(secrets: true)
end end
end end