Skip to content

Commit 73534c3

Browse files
committed
Add pre and post app boot hooks
Add two new hooks pre-app-boot and post-app-boot. They are analagous to the pre/post proxy reboot hooks. If the boot strategy deploys in groups, then the hooks are called once per group of hosts and `KAMAL_HOSTS` contains a comma delimited list of the hosts in that group. If all hosts are deployed to at once, then they are called once with `KAMAL_HOSTS` containing all the hosts. It is possible to have pauses between groups of hosts in the boot config, where this is the case the pause happens after the post-app-boot hook is called.
1 parent 5f04e42 commit 73534c3

File tree

17 files changed

+115
-97
lines changed

17 files changed

+115
-97
lines changed

lib/kamal/cli/app.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ def boot
1616
# Primary hosts and roles are returned first, so they can open the barrier
1717
barrier = Kamal::Cli::Healthcheck::Barrier.new
1818

19-
on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
20-
KAMAL.roles_on(host).each do |role|
21-
Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
19+
host_boot_groups.each do |hosts|
20+
host_list = Array(hosts).join(",")
21+
run_hook "pre-app-boot", hosts: host_list
22+
23+
on(hosts) do |host|
24+
KAMAL.roles_on(host).each do |role|
25+
Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
26+
end
2227
end
28+
29+
run_hook "post-app-boot", hosts: host_list
30+
sleep KAMAL.config.boot.wait if KAMAL.config.boot.wait
2331
end
2432

2533
# Tag once the app booted on all hosts
@@ -340,4 +348,8 @@ def with_lock_if_stopping
340348
yield
341349
end
342350
end
351+
352+
def host_boot_groups
353+
KAMAL.config.boot.limit ? KAMAL.hosts.each_slice(KAMAL.config.boot.limit).to_a : [ KAMAL.hosts ]
354+
end
343355
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..."
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..."

lib/kamal/commander.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,6 @@ def with_verbosity(level)
136136
SSHKit.config.output_verbosity = old_level
137137
end
138138

139-
def boot_strategy
140-
if config.boot.limit.present?
141-
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
142-
else
143-
{}
144-
end
145-
end
146-
147139
def holding_lock?
148140
self.holding_lock
149141
end

test/cli/app_test.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,20 @@ class CliAppTest < CliTestCase
3737
end
3838

3939
test "boot uses group strategy when specified" do
40-
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(2) # ensure locks dir, acquire & release lock
41-
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
40+
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").twice
41+
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ]).times(3)
4242

4343
# Strategy is used when booting the containers
44-
Kamal::Cli::App.any_instance.expects(:on).with([ "1.1.1.1" ], in: :groups, limit: 3, wait: 2).with_block_given
44+
Kamal::Cli::App.any_instance.expects(:on).with([ "1.1.1.1", "1.1.1.2", "1.1.1.3" ]).with_block_given
45+
Kamal::Cli::App.any_instance.expects(:on).with([ "1.1.1.4" ]).with_block_given
46+
Object.any_instance.expects(:sleep).with(2).twice
4547

46-
run_command("boot", config: :with_boot_strategy)
48+
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
49+
50+
run_command("boot", config: :with_boot_strategy, host: nil).tap do |output|
51+
assert_hook_ran "pre-app-boot", output, count: 2
52+
assert_hook_ran "post-app-boot", output, count: 2
53+
end
4754
end
4855

4956
test "boot errors don't leave lock in place" do

test/cli/build_test.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class CliBuildTest < CliTestCase
1111
test "push" do
1212
with_build_directory do |build_directory|
1313
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
14-
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" }
1514

1615
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
1716
.with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -22,7 +21,7 @@ class CliBuildTest < CliTestCase
2221
.returns("")
2322

2423
run_command("push", "--verbose").tap do |output|
25-
assert_hook_ran "pre-build", output, **hook_variables
24+
assert_hook_ran "pre-build", output
2625
assert_match /Cloning repo into build directory/, output
2726
assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
2827
assert_match /docker --version && docker buildx version/, output
@@ -68,11 +67,10 @@ class CliBuildTest < CliTestCase
6867

6968
test "push without clone" do
7069
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
71-
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" }
7270

7371
run_command("push", "--verbose", fixture: :without_clone).tap do |output|
7472
assert_no_match /Cloning repo into build directory/, output
75-
assert_hook_ran "pre-build", output, **hook_variables
73+
assert_hook_ran "pre-build", output
7674
assert_match /docker --version && docker buildx version/, output
7775
assert_match /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 . as .*@localhost/, output
7876
end

test/cli/cli_test_case.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ def stub_setup
4040
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
4141
end
4242

43-
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
44-
assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output
43+
def assert_hook_ran(hook, output, count: 1)
44+
regexp = ([ "/usr/bin/env .kamal/hooks/#{hook}" ] * count).join(".*")
45+
assert_match /#{regexp}/m, output
4546
end
4647

4748
def with_argv(*argv)

test/cli/main_test.rb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,16 @@ class CliMainTest < CliTestCase
5353
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
5454

5555
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
56-
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
5756

5857
run_command("deploy", "--verbose").tap do |output|
59-
assert_hook_ran "pre-connect", output, **hook_variables
58+
assert_hook_ran "pre-connect", output
6059
assert_match /Log into image registry/, output
6160
assert_match /Build and push app image/, output
62-
assert_hook_ran "pre-deploy", output, **hook_variables, secrets: true
61+
assert_hook_ran "pre-deploy", output
6362
assert_match /Ensure kamal-proxy is running/, output
6463
assert_match /Detect stale containers/, output
6564
assert_match /Prune old containers and images/, output
66-
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true, secrets: true
65+
assert_hook_ran "post-deploy", output
6766
end
6867
end
6968
end
@@ -205,14 +204,12 @@ class CliMainTest < CliTestCase
205204

206205
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
207206

208-
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
209-
210207
run_command("redeploy", "--verbose").tap do |output|
211-
assert_hook_ran "pre-connect", output, **hook_variables
208+
assert_hook_ran "pre-connect", output
212209
assert_match /Build and push app image/, output
213-
assert_hook_ran "pre-deploy", output, **hook_variables
210+
assert_hook_ran "pre-deploy", output
214211
assert_match /Running the pre-deploy hook.../, output
215-
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
212+
assert_hook_ran "post-deploy", output
216213
end
217214
end
218215

@@ -258,14 +255,13 @@ class CliMainTest < CliTestCase
258255
.returns("running").at_least_once # health check
259256

260257
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
261-
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" }
262258

263259
run_command("rollback", "--verbose", "123", config_file: "deploy_with_accessories").tap do |output|
264-
assert_hook_ran "pre-deploy", output, **hook_variables
260+
assert_hook_ran "pre-deploy", output
265261
assert_match "docker tag dhh/app:123 dhh/app:latest", output
266262
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
267263
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
268-
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
264+
assert_hook_ran "post-deploy", output
269265
end
270266
end
271267

test/commander_test.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,28 +104,6 @@ class CommanderTest < ActiveSupport::TestCase
104104
assert_equal [ "web", "workers" ], @kamal.roles_on("1.1.1.1").map(&:name)
105105
end
106106

107-
test "default group strategy" do
108-
assert_empty @kamal.boot_strategy
109-
end
110-
111-
test "specific limit group strategy" do
112-
configure_with(:deploy_with_boot_strategy)
113-
114-
assert_equal({ in: :groups, limit: 3, wait: 2 }, @kamal.boot_strategy)
115-
end
116-
117-
test "percentage-based group strategy" do
118-
configure_with(:deploy_with_percentage_boot_strategy)
119-
120-
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
121-
end
122-
123-
test "percentage-based group strategy limit is at least 1" do
124-
configure_with(:deploy_with_low_percentage_boot_strategy)
125-
126-
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
127-
end
128-
129107
test "try to match the primary role from a list of specific roles" do
130108
configure_with(:deploy_primary_web_role_override)
131109

test/configuration/boot_test.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require "test_helper"
2+
3+
class ConfigurationBootTest < ActiveSupport::TestCase
4+
test "no group strategy" do
5+
deploy = {
6+
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" },
7+
servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => [ "1.1.1.3", "1.1.1.4" ] }
8+
}
9+
10+
config = Kamal::Configuration.new(deploy)
11+
12+
assert_nil config.boot.limit
13+
assert_nil config.boot.wait
14+
end
15+
16+
test "specific limit group strategy" do
17+
deploy = {
18+
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" },
19+
servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => [ "1.1.1.3", "1.1.1.4" ] },
20+
boot: { "limit" => 3, "wait" => 2 }
21+
}
22+
23+
config = Kamal::Configuration.new(deploy)
24+
25+
assert_equal 3, config.boot.limit
26+
assert_equal 2, config.boot.wait
27+
end
28+
29+
test "percentage-based group strategy" do
30+
deploy = {
31+
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" },
32+
servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => [ "1.1.1.3", "1.1.1.4" ] },
33+
boot: { "limit" => "50%", "wait" => 2 }
34+
}
35+
36+
config = Kamal::Configuration.new(deploy)
37+
38+
assert_equal 2, config.boot.limit
39+
assert_equal 2, config.boot.wait
40+
end
41+
42+
test "percentage-based group strategy limit is at least 1" do
43+
deploy = {
44+
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, builder: { "arch" => "amd64" },
45+
servers: { "web" => [ "1.1.1.1", "1.1.1.2" ], "workers" => [ "1.1.1.3", "1.1.1.4" ] },
46+
boot: { "limit" => "1%", "wait" => 2 }
47+
}
48+
49+
config = Kamal::Configuration.new(deploy)
50+
51+
assert_equal 1, config.boot.limit
52+
assert_equal 2, config.boot.wait
53+
end
54+
end

0 commit comments

Comments
 (0)