From 1522d94ac91abf839a2f3fe76f312fc0c3a59d15 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 4 Sep 2024 16:24:10 +0100 Subject: [PATCH] Pass secrets to pre/post deploy hooks --- lib/kamal/cli/main.rb | 12 ++++++------ lib/kamal/commands/hook.rb | 7 +++++-- lib/kamal/secrets.rb | 11 +++++++++-- test/cli/cli_test_case.rb | 3 ++- test/cli/main_test.rb | 38 ++++++++++++++++++++------------------ test/commands/hook_test.rb | 15 +++++++++++++++ 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/lib/kamal/cli/main.rb b/lib/kamal/cli/main.rb index 9fa9ba92..b1f01238 100644 --- a/lib/kamal/cli/main.rb +++ b/lib/kamal/cli/main.rb @@ -33,7 +33,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end with_lock do - run_hook "pre-deploy" + run_hook "pre-deploy", secrets: true say "Ensure Traefik is running...", :magenta invoke "kamal:cli:traefik:boot", [], invoke_options @@ -48,7 +48,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", runtime: runtime.round + run_hook "post-deploy", secrets: true, runtime: runtime.round end desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" @@ -66,7 +66,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end with_lock do - run_hook "pre-deploy" + run_hook "pre-deploy", secrets: true say "Detect stale containers...", :magenta invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true) @@ -75,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", runtime: runtime.round + run_hook "post-deploy", secrets: true, runtime: runtime.round end desc "rollback [VERSION]", "Rollback app to VERSION" @@ -89,7 +89,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base old_version = nil if container_available?(version) - run_hook "pre-deploy" + run_hook "pre-deploy", secrets: true invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version) rolled_back = true @@ -99,7 +99,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base end end - run_hook "post-deploy", runtime: runtime.round if rolled_back + run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back end desc "details", "Show details about all containers" diff --git a/lib/kamal/commands/hook.rb b/lib/kamal/commands/hook.rb index 66fe8b8c..eb710d5e 100644 --- a/lib/kamal/commands/hook.rb +++ b/lib/kamal/commands/hook.rb @@ -1,6 +1,9 @@ class Kamal::Commands::Hook < Kamal::Commands::Base - def run(hook, **details) - [ hook_file(hook), env: tags(**details).env ] + def run(hook, secrets: false, **details) + env = tags(**details).env + env.merge!(config.secrets.to_h) if secrets + + [ hook_file(hook), env: env ] end def hook_exists?(hook) diff --git a/lib/kamal/secrets.rb b/lib/kamal/secrets.rb index 151dd3b2..25d24934 100644 --- a/lib/kamal/secrets.rb +++ b/lib/kamal/secrets.rb @@ -6,8 +6,7 @@ class Kamal::Secrets end def [](key) - @secrets ||= parse_secrets - @secrets.fetch(key) + secrets.fetch(key) rescue KeyError if secrets_file raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_file}" @@ -16,7 +15,15 @@ class Kamal::Secrets end end + def to_h + secrets + end + private + def secrets + @secrets ||= parse_secrets + end + def parse_secrets if secrets_file interrupting_parent_on_error { Dotenv.parse(secrets_file) } diff --git a/test/cli/cli_test_case.rb b/test/cli/cli_test_case.rb index c522a32f..3f3e9294 100644 --- a/test/cli/cli_test_case.rb +++ b/test/cli/cli_test_case.rb @@ -40,7 +40,7 @@ class CliTestCase < ActiveSupport::TestCase .with(:docker, :buildx, :inspect, "kamal-local-docker-container") end - def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) + def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false) whoami = `whoami`.chomp performer = Kamal::Git.email.presence || whoami service = service_version.split("@").first @@ -58,6 +58,7 @@ class CliTestCase < ActiveSupport::TestCase 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 diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 8b9f129c..c742afe9 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -43,27 +43,29 @@ class CliMainTest < CliTestCase end test "deploy" do - invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true } + with_test_secrets("secrets" => "DB_PASSWORD=secret") do + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true } - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) - Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options) - Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) - hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" } + Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) + hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" } - run_command("deploy", "--verbose").tap do |output| - assert_hook_ran "pre-connect", output, **hook_variables - assert_match /Log into image registry/, output - assert_match /Build and push app image/, output - assert_hook_ran "pre-deploy", output, **hook_variables - assert_match /Ensure Traefik is running/, output - assert_match /Detect stale containers/, output - assert_match /Prune old containers and images/, output - assert_hook_ran "post-deploy", output, **hook_variables, runtime: true + run_command("deploy", "--verbose").tap do |output| + assert_hook_ran "pre-connect", output, **hook_variables + assert_match /Log into image registry/, output + assert_match /Build and push app image/, output + assert_hook_ran "pre-deploy", output, **hook_variables, secrets: true + assert_match /Ensure Traefik is running/, output + assert_match /Detect stale containers/, output + assert_match /Prune old containers and images/, output + assert_hook_ran "post-deploy", output, **hook_variables, runtime: true, secrets: true + end end end diff --git a/test/commands/hook_test.rb b/test/commands/hook_test.rb index 60438c66..f6234d6a 100644 --- a/test/commands/hook_test.rb +++ b/test/commands/hook_test.rb @@ -39,6 +39,21 @@ class CommandsHookTest < ActiveSupport::TestCase ], new_command(hooks_path: "custom/hooks/path").run("foo") end + test "hook with secrets" do + with_test_secrets("secrets" => "DB_PASSWORD=secret") do + assert_equal [ + ".kamal/hooks/foo", + { env: { + "KAMAL_RECORDED_AT" => @recorded_at, + "KAMAL_PERFORMER" => @performer, + "KAMAL_VERSION" => "123", + "KAMAL_SERVICE_VERSION" => "app@123", + "KAMAL_SERVICE" => "app", + "DB_PASSWORD" => "secret" } } + ], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true) + end + end + private def new_command(**extra_config) Kamal::Commands::Hook.new(Kamal::Configuration.new(@config.merge(**extra_config), version: "123"))