Add a pre-init hook
The hook is run before the environment is loaded or the config is parsed. This makes it a bit of a special case - it doesn't have the usual KAMAL_XYZ environment variables, as we haven't parsed the config. The use case for this is to do auth checking or setup. So for example we can confirm you are logged in to a secret manager, and then you can directly call it to load your secrets in the .kamal/.env file using .dotenv's [command substitution](https://github.com/bkeepers/dotenv?tab=readme-ov-file#command-substitution).
This commit is contained in:
@@ -22,9 +22,21 @@ module Kamal::Cli
|
|||||||
|
|
||||||
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
||||||
|
|
||||||
|
@@ran_pre_init_hook = false
|
||||||
|
class << self
|
||||||
|
def ran_pre_init_hook
|
||||||
|
@@ran_pre_init_hook
|
||||||
|
end
|
||||||
|
|
||||||
|
def ran_pre_init_hook=(value)
|
||||||
|
@@ran_pre_init_hook = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(*)
|
||||||
super
|
super
|
||||||
@original_env = ENV.to_h.dup
|
@original_env = ENV.to_h.dup
|
||||||
|
run_pre_init_hook
|
||||||
load_env
|
load_env
|
||||||
initialize_commander(options_with_subcommand_class_options)
|
initialize_commander(options_with_subcommand_class_options)
|
||||||
end
|
end
|
||||||
@@ -176,8 +188,23 @@ module Kamal::Cli
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run_pre_init_hook
|
||||||
|
unless self.class.ran_pre_init_hook
|
||||||
|
hook = "pre-init"
|
||||||
|
if run_hook?(hook)
|
||||||
|
say "Running the #{hook} hook...", :magenta
|
||||||
|
run_locally do
|
||||||
|
execute *Kamal::Hooks.file(hook), verbosity: :debug
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.class.ran_pre_init_hook = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def run_hook(hook, **extra_details)
|
def run_hook(hook, **extra_details)
|
||||||
if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
|
if run_hook?(hook)
|
||||||
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
|
||||||
@@ -189,6 +216,10 @@ module Kamal::Cli
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run_hook?(hook)
|
||||||
|
!options[:skip_hooks] && Kamal::Hooks.exists?(hook)
|
||||||
|
end
|
||||||
|
|
||||||
def on(*args, &block)
|
def on(*args, &block)
|
||||||
if !KAMAL.connected?
|
if !KAMAL.connected?
|
||||||
run_hook "pre-connect"
|
run_hook "pre-connect"
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
class Kamal::Commands::Hook < Kamal::Commands::Base
|
class Kamal::Commands::Hook < Kamal::Commands::Base
|
||||||
def run(hook, **details)
|
def run(hook, **details)
|
||||||
[ hook_file(hook), env: tags(**details).env ]
|
[ Kamal::Hooks.file(hook), env: tags(**details).env ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def hook_exists?(hook)
|
|
||||||
Pathname.new(hook_file(hook)).exist?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def hook_file(hook)
|
|
||||||
File.join(config.hooks_path, hook)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require "erb"
|
|||||||
require "net/ssh/proxy/jump"
|
require "net/ssh/proxy/jump"
|
||||||
|
|
||||||
class Kamal::Configuration
|
class Kamal::Configuration
|
||||||
delegate :service, :image, :labels, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
delegate :service, :image, :labels, :stop_wait_time, to: :raw_config, allow_nil: true
|
||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_reader :destination, :raw_config
|
attr_reader :destination, :raw_config
|
||||||
@@ -208,10 +208,6 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hooks_path
|
|
||||||
raw_config.hooks_path || ".kamal/hooks"
|
|
||||||
end
|
|
||||||
|
|
||||||
def asset_path
|
def asset_path
|
||||||
raw_config.asset_path
|
raw_config.asset_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ env:
|
|||||||
# To configure this, set the path to the assets:
|
# To configure this, set the path to the assets:
|
||||||
asset_path: /path/to/assets
|
asset_path: /path/to/assets
|
||||||
|
|
||||||
# Path to hooks, defaults to `.kamal/hooks`
|
|
||||||
# See https://kamal-deploy.org/docs/hooks for more information
|
|
||||||
hooks_path: /user_home/kamal/hooks
|
|
||||||
|
|
||||||
# Require destinations
|
# Require destinations
|
||||||
#
|
#
|
||||||
# Whether deployments require a destination to be specified, defaults to `false`
|
# Whether deployments require a destination to be specified, defaults to `false`
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class CliBuildTest < CliTestCase
|
|||||||
|
|
||||||
test "push" do
|
test "push" do
|
||||||
with_build_directory do |build_directory|
|
with_build_directory do |build_directory|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
@@ -70,7 +70,7 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "push without clone" do
|
test "push without clone" do
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||||
|
|
||||||
run_command("push", "--verbose", fixture: :without_clone).tap do |output|
|
run_command("push", "--verbose", fixture: :without_clone).tap do |output|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
private
|
private
|
||||||
def fail_hook(hook)
|
def fail_hook(hook)
|
||||||
@executions = []
|
@executions = []
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*args| @executions << args; args != [ ".kamal/hooks/#{hook}" ] }
|
.with { |*args| @executions << args; args != [ ".kamal/hooks/#{hook}" ] }
|
||||||
|
|||||||
@@ -58,10 +58,11 @@ class CliMainTest < CliTestCase
|
|||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
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:prune:all", [], invoke_options)
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
|
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|
|
run_command("deploy", "--verbose").tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env .kamal/hooks/pre-init", output
|
||||||
assert_hook_ran "pre-connect", output, **hook_variables
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
@@ -237,7 +238,7 @@ class CliMainTest < CliTestCase
|
|||||||
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: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:app:boot", [], invoke_options)
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
|
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
||||||
|
|
||||||
@@ -298,7 +299,7 @@ class CliMainTest < CliTestCase
|
|||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-version-to-rollback$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
.returns("unhealthy").at_least_once # health check
|
.returns("unhealthy").at_least_once # health check
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
||||||
|
|
||||||
run_command("rollback", "--verbose", "123", config_file: "deploy_with_accessories").tap do |output|
|
run_command("rollback", "--verbose", "123", config_file: "deploy_with_accessories").tap do |output|
|
||||||
@@ -396,7 +397,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init" do
|
test "init" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
Pathname.any_instance.expects(:exist?).returns(false).times(4)
|
||||||
Pathname.any_instance.stubs(:mkpath)
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
@@ -409,7 +410,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with existing config" do
|
test "init with existing config" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
||||||
|
|
||||||
run_command("init").tap do |output|
|
run_command("init").tap do |output|
|
||||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||||
@@ -417,7 +418,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with bundle option" do
|
test "init with bundle option" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).times(4)
|
Pathname.any_instance.expects(:exist?).returns(false).times(5)
|
||||||
Pathname.any_instance.stubs(:mkpath)
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
@@ -434,7 +435,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with bundle option and existing binstub" do
|
test "init with bundle option and existing binstub" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
Pathname.any_instance.expects(:exist?).returns(true).times(5)
|
||||||
Pathname.any_instance.stubs(:mkpath)
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
@@ -475,7 +476,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "envify with skip_push" do
|
test "envify with skip_push" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(1)
|
Pathname.any_instance.expects(:exist?).returns(true).times(2)
|
||||||
File.expects(:read).with(".kamal/.env.erb").returns("HELLO=<%= 'world' %>")
|
File.expects(:read).with(".kamal/.env.erb").returns("HELLO=<%= 'world' %>")
|
||||||
File.expects(:write).with(".kamal/.env", "HELLO=world", perm: 0600)
|
File.expects(:write).with(".kamal/.env", "HELLO=world", perm: 0600)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ class CliServerTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Hooks.stubs(:exists?).returns(true)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-init", anything).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-connect", anything).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-connect", anything).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class CommandsHookTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "run with custom hooks_path" do
|
test "run with custom hooks_path" do
|
||||||
|
ENV["KAMAL_HOOKS_PATH"] = "custom/hooks/path"
|
||||||
assert_equal [
|
assert_equal [
|
||||||
"custom/hooks/path/foo",
|
"custom/hooks/path/foo",
|
||||||
{ env: {
|
{ env: {
|
||||||
@@ -36,7 +37,9 @@ class CommandsHookTest < ActiveSupport::TestCase
|
|||||||
"KAMAL_VERSION" => "123",
|
"KAMAL_VERSION" => "123",
|
||||||
"KAMAL_SERVICE_VERSION" => "app@123",
|
"KAMAL_SERVICE_VERSION" => "app@123",
|
||||||
"KAMAL_SERVICE" => "app" } }
|
"KAMAL_SERVICE" => "app" } }
|
||||||
], new_command(hooks_path: "custom/hooks/path").run("foo")
|
], new_command.run("foo")
|
||||||
|
ensure
|
||||||
|
ENV.delete("KAMAL_HOOKS_PATH")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "wrong root types" do
|
test "wrong root types" do
|
||||||
[ :service, :image, :asset_path, :hooks_path, :primary_role, :minimum_version, :run_directory ].each do |key|
|
[ :service, :image, :asset_path, :primary_role, :minimum_version, :run_directory ].each do |key|
|
||||||
assert_error "#{key}: should be a string", **{ key => [] }
|
assert_error "#{key}: should be a string", **{ key => [] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ class MainTest < IntegrationTest
|
|||||||
|
|
||||||
kamal :deploy
|
kamal :deploy
|
||||||
assert_app_is_up version: first_version
|
assert_app_is_up version: first_version
|
||||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
assert_hooks_ran "pre-init", "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||||
assert_envs version: first_version
|
assert_envs version: first_version
|
||||||
|
|
||||||
second_version = update_app_rev
|
second_version = update_app_rev
|
||||||
|
|
||||||
kamal :redeploy
|
kamal :redeploy
|
||||||
assert_app_is_up version: second_version
|
assert_app_is_up version: second_version
|
||||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
assert_hooks_ran "pre-init", "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||||
|
|
||||||
assert_accumulated_assets first_version, second_version
|
assert_accumulated_assets first_version, second_version
|
||||||
|
|
||||||
kamal :rollback, first_version
|
kamal :rollback, first_version
|
||||||
assert_hooks_ran "pre-connect", "pre-deploy", "post-deploy"
|
assert_hooks_ran "pre-init", "pre-connect", "pre-deploy", "post-deploy"
|
||||||
assert_app_is_up version: first_version
|
assert_app_is_up version: first_version
|
||||||
|
|
||||||
details = kamal :details, capture: true
|
details = kamal :details, capture: true
|
||||||
@@ -54,7 +54,7 @@ class MainTest < IntegrationTest
|
|||||||
kamal :deploy
|
kamal :deploy
|
||||||
|
|
||||||
assert_app_is_up version: version
|
assert_app_is_up version: version
|
||||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
assert_hooks_ran "pre-init", "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||||
assert_container_running host: :vm3, name: "app-workers-#{version}"
|
assert_container_running host: :vm3, name: "app-workers-#{version}"
|
||||||
|
|
||||||
second_version = update_app_rev
|
second_version = update_app_rev
|
||||||
@@ -65,7 +65,7 @@ class MainTest < IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "config" do
|
test "config" do
|
||||||
config = YAML.load(kamal(:config, capture: true))
|
config = YAML.load(kamal(:config, "-q", capture: true))
|
||||||
version = latest_app_version
|
version = latest_app_version
|
||||||
|
|
||||||
assert_equal [ "web" ], config[:roles]
|
assert_equal [ "web" ], config[:roles]
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ end
|
|||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
include ActiveSupport::Testing::Stream
|
include ActiveSupport::Testing::Stream
|
||||||
|
|
||||||
|
setup do
|
||||||
|
Kamal::Cli::Base.ran_pre_init_hook = false
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def stdouted
|
def stdouted
|
||||||
capture(:stdout) { yield }.strip
|
capture(:stdout) { yield }.strip
|
||||||
|
|||||||
Reference in New Issue
Block a user