From 2c1d6ed8916e9f93a5a2639092f8e28f9214d1b8 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 14 May 2025 15:55:54 +0100 Subject: [PATCH 1/7] Run pre-connect hooks before building They might be needed for remote builds or the pre-build hook. --- lib/kamal/cli/build.rb | 4 ++++ test/cli/build_test.rb | 1 + 2 files changed, 5 insertions(+) diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index b9b6a5d5..2327943e 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -14,6 +14,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base def push cli = self + # Ensure pre-connect hooks run before the build, they may needed for a remote builder + # or the pre-build hooks. + pre_connect_if_required + ensure_docker_installed login_to_registry_locally diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index c0a236ad..0bdd6b65 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -21,6 +21,7 @@ class CliBuildTest < CliTestCase .returns("") run_command("push", "--verbose").tap do |output| + assert_hook_ran "pre-connect", output assert_hook_ran "pre-build", output assert_match /Cloning repo into build directory/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output From b9e5ce7ca7a93d1b37d8bd37365a251b27ea684b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 09:51:40 +0100 Subject: [PATCH 2/7] Ensure primary_role app hosts are sorted first When booting non-primary role hosts we will always wait for a primary role host to boor first. So when booting in groups, if there are no primary role hosts in the first batch, then booting will stall. Sort primary role app_hosts first to avoid this. Fixes: https://github.com/basecamp/kamal/issues/1553 --- lib/kamal/commander/specifics.rb | 8 ++++++-- test/commander_test.rb | 6 ++++++ .../deploy_with_roles_workers_primary.yml | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/deploy_with_roles_workers_primary.yml diff --git a/lib/kamal/commander/specifics.rb b/lib/kamal/commander/specifics.rb index e609baba..d61f6b28 100644 --- a/lib/kamal/commander/specifics.rb +++ b/lib/kamal/commander/specifics.rb @@ -11,7 +11,7 @@ class Kamal::Commander::Specifics @primary_role = primary_or_first_role(roles_on(primary_host)) stable_sort!(roles) { |role| role == primary_role ? 0 : 1 } - stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 } + sort_primary_role_hosts_first!(hosts) end def roles_on(host) @@ -19,7 +19,7 @@ class Kamal::Commander::Specifics end def app_hosts - config.app_hosts & specified_hosts + @app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts) end def proxy_hosts @@ -55,4 +55,8 @@ class Kamal::Commander::Specifics specified_hosts end end + + def sort_primary_role_hosts_first!(hosts) + stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 } + end end diff --git a/test/commander_test.rb b/test/commander_test.rb index 4a3aa2e2..84115f3a 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -149,6 +149,12 @@ class CommanderTest < ActiveSupport::TestCase assert_equal [], @kamal.accessory_hosts end + test "primary role hosts are first" do + configure_with(:deploy_with_roles_workers_primary) + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts + assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.app_hosts + end + private def configure_with(variant) @kamal = Kamal::Commander.new.tap do |kamal| diff --git a/test/fixtures/deploy_with_roles_workers_primary.yml b/test/fixtures/deploy_with_roles_workers_primary.yml new file mode 100644 index 00000000..4e208c6b --- /dev/null +++ b/test/fixtures/deploy_with_roles_workers_primary.yml @@ -0,0 +1,19 @@ +service: app +image: dhh/app +servers: + workers: + - 1.1.1.1 + - 1.1.1.2 + web: + - 1.1.1.3 + - 1.1.1.4 +env: + REDIS_URL: redis://x/y +registry: + server: registry.digitalocean.com + username: user + password: pw +builder: + arch: amd64 +deploy_timeout: 1 +primary_role: workers From 7b1439c3c645ea856e8b8f407409fa1adf176d36 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 10:14:52 +0100 Subject: [PATCH 3/7] Update per-role proxy docs Clarify that proxy: true/proxy: false only belong in the role config, not at the root level. --- lib/kamal/configuration/docs/proxy.yml | 32 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/kamal/configuration/docs/proxy.yml b/lib/kamal/configuration/docs/proxy.yml index 49c11ac8..a127bfea 100644 --- a/lib/kamal/configuration/docs/proxy.yml +++ b/lib/kamal/configuration/docs/proxy.yml @@ -10,11 +10,6 @@ # They are application-specific, so they are not shared when multiple applications # run on the same proxy. # -# The proxy is enabled by default on the primary role but can be disabled by -# setting `proxy: false`. -# -# It is disabled by default on all other roles but can be enabled by setting -# `proxy: true` or providing a proxy configuration. proxy: # Hosts @@ -113,3 +108,30 @@ proxy: response_headers: - X-Request-ID - X-Request-Start + +# Enabling/disabling the proxy on roles +# +# The proxy is enabled by default on the primary role but can be disabled by +# setting `proxy: false` in the primary role's configuration. +# +# ```yaml +# servers: +# web: +# hosts: +# - ... +# proxy: false +# ``` +# +# It is disabled by default on all other roles but can be enabled by setting +# `proxy: true` or providing a proxy configuration for that role. +# +# ```yaml +# servers: +# web: +# hosts: +# - ... +# web2: +# hosts: +# - ... +# proxy: true +# ``` From 87965281a3abf036110a667e7a3e13b9b0e79ac8 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 14:33:05 +0100 Subject: [PATCH 4/7] Default the proxy config is it is nil Instead of checking for the proxy key, we'll set the config to {} if it is nil in the Kamal::Configuration::Proxy initializer. This is a bit cleaner, and maybe it will help with https://github.com/basecamp/kamal/issues/1555 if somehow @raw_config.key?(:proxy) is false but @raw_config.proxy is not nil. --- lib/kamal/configuration.rb | 2 +- lib/kamal/configuration/proxy.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 26450170..6435fa57 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -63,7 +63,7 @@ class Kamal::Configuration @env = Env.new(config: @raw_config.env || {}, secrets: secrets) @logging = Logging.new(logging_config: @raw_config.logging) - @proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {}) + @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy) @proxy_boot = Proxy::Boot.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index b5afbaae..ccb4ac42 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -11,6 +11,7 @@ class Kamal::Configuration::Proxy def initialize(config:, proxy_config:, context: "proxy") @config = config @proxy_config = proxy_config + @proxy_config = {} if @proxy_config.nil? validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context end From ad21c7e984bde535b25b651d89a9da7b14863a75 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 14:45:19 +0100 Subject: [PATCH 5/7] Don't deploy on proxy reboot It shouldn't be necessary to deploy the app on proxy reboot. When there are multiple apps using the same proxy we'll only deploy the one we run the reboot command from, so we don't always reboot anyway. --- lib/kamal/cli/proxy.rb | 12 ------------ test/cli/proxy_test.rb | 22 ---------------------- 2 files changed, 34 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 73707572..0e00af6c 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -120,18 +120,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base execute *KAMAL.proxy.ensure_apps_config_directory execute *KAMAL.proxy.run - - KAMAL.roles_on(host).select(&:running_proxy?).each do |role| - app = KAMAL.app(role: role, host: host) - - version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip - endpoint = capture_with_info(*app.container_id_for_version(version)).strip - - if endpoint.present? - info "Deploying #{endpoint} for role `#{role}` on #{host}..." - execute *app.deploy(target: endpoint) - end - end end run_hook "post-proxy-reboot", hosts: host_list end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index fd131984..e6b0eaea 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -44,42 +44,20 @@ class CliProxyTest < CliTestCase end test "reboot" do - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") - .returns("abcdefabcdef") - .at_least_once - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with { |*args| args[0..1] == [ :sh, "-c" ] } - .returns("123") - .at_least_once - run_command("reboot", "-y").tap do |output| assert_match "docker container stop kamal-proxy on 1.1.1.1", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.1", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output assert_match "docker container stop kamal-proxy on 1.1.1.2", output assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config on 1.1.1.2", output - assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output end end test "reboot --rolling" do - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet") - .returns("abcdefabcdef") - .at_least_once - - SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with { |*args| args[0..1] == [ :sh, "-c" ] } - .returns("123") - .at_least_once - run_command("reboot", "--rolling", "-y").tap do |output| assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output end From 22e7243b100a0a5d2f36486d139e18842a67d345 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 15:15:29 +0100 Subject: [PATCH 6/7] Bump version for 2.6.1 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bcc38f0a..3fb94ec5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.6.0) + kamal (2.6.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 9525c872..d9029c54 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.6.0" + VERSION = "2.6.1" end From 30d630ce4df2f2815a494db6be6d91cc7bccc216 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 15 May 2025 15:21:13 +0100 Subject: [PATCH 7/7] Drop Ruby 3.1 from the test matrix It is EOL since 2025-03-26. --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8da7d39c..4e0358b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,16 +26,12 @@ jobs: fail-fast: false matrix: ruby-version: - - "3.1" - "3.2" - "3.3" - "3.4" gemfile: - Gemfile - gemfiles/rails_edge.gemfile - exclude: - - ruby-version: "3.1" - gemfile: gemfiles/rails_edge.gemfile name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }} runs-on: ubuntu-latest env: