From c9a755bde694a562564f9891fe3e94a978e87a46 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 15 Apr 2025 12:04:06 +0100 Subject: [PATCH 1/5] Extract echo_boot_config/docker_run methods --- lib/kamal/commands/proxy.rb | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 6ca87e02..236ce0c1 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -2,14 +2,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils def run - pipe \ - [ :echo, "\$\(#{get_boot_options.join(" ")}\) #{config.proxy_image}" ], - xargs(docker(:run, - "--name", container_name, - "--network", "kamal", - "--detach", - "--restart", "unless-stopped", - "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy")) + pipe echo_boot_config, xargs(docker_run) end def start @@ -84,4 +77,18 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base def container_name config.proxy_container_name end + + def echo_boot_config + [ :echo, "\$\(#{get_boot_options.join(" ")}\) #{config.proxy_image}" ] + end + + def docker_run + docker \ + :run, + "--name", container_name, + "--network", "kamal", + "--detach", + "--restart", "unless-stopped", + "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy" + end end From 85320dbc518b96d2341c70be76ca5b588b48cf00 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 16 Apr 2025 16:01:34 +0100 Subject: [PATCH 2/5] Custom proxy image registry, repo and version Use the --registry, --repository and --image_version options of `kamal proxy boot_config set` to change the kamal-proxy image used. We'll still insist that the image version is at least as high as the minimum. --- lib/kamal/cli/proxy.rb | 35 +++++++- lib/kamal/commands/base.rb | 4 + lib/kamal/commands/proxy.rb | 32 +++++-- lib/kamal/configuration.rb | 20 ++++- test/cli/proxy_test.rb | 84 +++++++++++++++---- test/commands/proxy_test.rb | 34 ++++++-- .../deployer/app/.kamal/hooks/pre-deploy | 3 + .../config/deploy.yml | 2 +- .../app_with_roles/.kamal/hooks/pre-deploy | 3 + .../app_with_traefik/.kamal/hooks/pre-deploy | 5 +- test/integration/docker/deployer/setup.sh | 1 + test/integration/integration_test.rb | 10 +-- test/integration/proxy_test.rb | 4 +- 13 files changed, 197 insertions(+), 40 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index f73177ad..9ce8373e 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -27,6 +27,9 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host" option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host" option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs" + option :registry, type: :string, default: nil, desc: "Registry to use for the proxy image" + option :repository, type: :string, default: nil, desc: "Repository for the proxy image" + option :image_version, type: :string, default: nil, desc: "Version of the proxy to run" option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2" def boot_config(subcommand) case subcommand @@ -37,17 +40,43 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base *options[:docker_options].map { |option| "--#{option}" } ] + default_boot_options = [ + *(KAMAL.config.proxy_publish_args(Kamal::Configuration::PROXY_HTTP_PORT, Kamal::Configuration::PROXY_HTTPS_PORT, nil)), + *(KAMAL.config.proxy_logging_args(Kamal::Configuration::PROXY_LOG_MAX_SIZE)), + ] + + image = [ options[:registry].presence, options[:repository] || KAMAL.config.proxy_repository_name, KAMAL.config.proxy_image_name ].compact.join("/") + image_version = options[:image_version] + on(KAMAL.proxy_hosts) do |host| execute(*KAMAL.proxy.ensure_proxy_directory) - upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file + if boot_options != default_boot_options + upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file + else + execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false + end + + if image != KAMAL.config.proxy_image_default + upload! StringIO.new(image), KAMAL.config.proxy_image_file + else + execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false + end + + if image_version + upload! StringIO.new(image_version), KAMAL.config.proxy_image_version_file + else + execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false + end end when "get" on(KAMAL.proxy_hosts) do |host| - puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.get_boot_options)}" + puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.boot_config)}" end when "reset" on(KAMAL.proxy_hosts) do |host| - execute *KAMAL.proxy.reset_boot_options + execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false + execute *KAMAL.proxy.reset_image, raise_on_non_zero_exit: false + execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false end else raise ArgumentError, "Unknown boot_config subcommand #{subcommand}" diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 535d17c0..ba3b86ba 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -68,6 +68,10 @@ module Kamal::Commands combine *commands, by: "||" end + def substitute(*commands) + "\$\(#{commands.join(" ")}\)" + end + def xargs(command) [ :xargs, command ].flatten end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 236ce0c1..25fc1efe 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -2,7 +2,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base delegate :argumentize, :optionize, to: Kamal::Utils def run - pipe echo_boot_config, xargs(docker_run) + pipe boot_config, xargs(docker_run) end def start @@ -24,7 +24,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base def version pipe \ docker(:inspect, container_name, "--format '{{.Config.Image}}'"), - [ :cut, "-d:", "-f2" ] + [ :awk, "-F:", "'{print \$NF}'" ] end def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) @@ -65,21 +65,41 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base remove_directory config.proxy_directory end - def get_boot_options - combine [ :cat, config.proxy_options_file, "2>", "/dev/null" ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||" + def boot_config + [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)}" ] + end + + def read_boot_options + read_file(config.proxy_options_file, default: config.proxy_options_default.join(" ")) + end + + def read_image + read_file(config.proxy_image_file, default: config.proxy_image_default) + end + + def read_image_version + read_file(config.proxy_image_version_file, default: Kamal::Configuration::PROXY_MINIMUM_VERSION) end def reset_boot_options remove_file config.proxy_options_file end + def reset_image + remove_file config.proxy_image_file + end + + def reset_image_version + remove_file config.proxy_image_version_file + end + private def container_name config.proxy_container_name end - def echo_boot_config - [ :echo, "\$\(#{get_boot_options.join(" ")}\) #{config.proxy_image}" ] + def read_file(file, default: nil) + combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||" end def docker_run diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 1c0b79cf..1e9cd667 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -261,8 +261,16 @@ class Kamal::Configuration [ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ] end - def proxy_image - "basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}" + def proxy_repository_name + "basecamp" + end + + def proxy_image_name + "kamal-proxy" + end + + def proxy_image_default + "#{proxy_repository_name}/#{proxy_image_name}" end def proxy_container_name @@ -277,6 +285,14 @@ class Kamal::Configuration File.join proxy_directory, "options" end + def proxy_image_file + File.join proxy_directory, "image" + end + + def proxy_image_version_file + File.join proxy_directory, "image_version" + end + def to_h { roles: role_names, diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 4880a839..cd9cd5b9 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,21 +4,21 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end end test "boot old version" do Thread.report_on_exception = false SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") .returns("v0.0.1") .at_least_once exception = assert_raises do run_command("boot").tap do |output| assert_match "docker login", output - assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end end @@ -30,13 +30,13 @@ class CliProxyTest < CliTestCase test "boot correct version" do Thread.report_on_exception = false SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) .at_least_once run_command("boot").tap do |output| assert_match "docker login", output - assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "docker container start kamal-proxy || 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output end ensure Thread.report_on_exception = false @@ -56,12 +56,12 @@ class CliProxyTest < CliTestCase 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 "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy 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 "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy on 1.1.1.2", 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy 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 @@ -179,7 +179,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -196,7 +196,7 @@ class CliProxyTest < CliTestCase assert_match "/usr/bin/env mkdir -p .kamal", output assert_match "docker network create kamal", output assert_match "docker login -u [REDACTED] -p [REDACTED]", output - assert_match "docker container start kamal-proxy || echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output + assert_match "docker container start kamal-proxy || 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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", output assert_match "/usr/bin/env mkdir -p .kamal", output assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output @@ -218,7 +218,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("12345678") SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2") + .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) @@ -238,8 +238,9 @@ class CliProxyTest < CliTestCase run_command("boot_config", "set").tap do |output| %w[ 1.1.1.1 1.1.1.2 ].each do |host| assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output - assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output - end + assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end end end @@ -248,6 +249,8 @@ class CliProxyTest < CliTestCase %w[ 1.1.1.1 1.1.1.2 ].each do |host| assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output assert_match "Uploading \"--log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end end end @@ -257,6 +260,8 @@ class CliProxyTest < CliTestCase %w[ 1.1.1.1 1.1.1.2 ].each do |host| assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=100m\" to .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end end end @@ -266,6 +271,8 @@ class CliProxyTest < CliTestCase %w[ 1.1.1.1 1.1.1.2 ].each do |host| assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end end end @@ -310,19 +317,64 @@ class CliProxyTest < CliTestCase %w[ 1.1.1.1 1.1.1.2 ].each do |host| assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + end + end + end + + test "boot_config set registry" do + run_command("boot_config", "set", "--registry", "myreg").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output + assert_match "Uploading \"myreg/basecamp/kamal-proxy\" to .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + end + end + end + + test "boot_config set repository" do + run_command("boot_config", "set", "--repository", "myrepo").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output + assert_match "Uploading \"myrepo/kamal-proxy\" to .kamal/proxy/image on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + end + end + end + + test "boot_config set image_version" do + run_command("boot_config", "set", "--image_version", "0.9.9").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/options on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/image on #{host}", output + assert_match "Uploading \"0.9.9\" to .kamal/proxy/image_version on #{host}", output + end + end + end + + test "boot_config set all" do + run_command("boot_config", "set", "--docker_options", "label=foo=bar", "--registry", "myreg", "--repository", "myrepo", "--image_version", "0.9.9").tap do |output| + %w[ 1.1.1.1 1.1.1.2 ].each do |host| + assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar\" to .kamal/proxy/options on #{host}", output + assert_match "Uploading \"myreg/myrepo/kamal-proxy\" to .kamal/proxy/image on #{host}", output + assert_match "Uploading \"0.9.9\" to .kamal/proxy/image_version on #{host}", output end end end test "boot_config get" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .with(:cat, ".kamal/proxy/options", "2>", "/dev/null", "||", :echo, "\"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"") - .returns("--publish 80:80 --publish 8443:443 --label=foo=bar") + .with(: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 \"v0.8.7\")") + .returns("--publish 80:80 --publish 8443:443 --label=foo=bar basecamp/kamal-proxy:v1.0.0") .twice run_command("boot_config", "get").tap do |output| - assert_match "Host 1.1.1.1: --publish 80:80 --publish 8443:443 --label=foo=bar", output - assert_match "Host 1.1.1.2: --publish 80:80 --publish 8443:443 --label=foo=bar", output + assert_match "Host 1.1.1.1: --publish 80:80 --publish 8443:443 --label=foo=bar basecamp/kamal-proxy:v1.0.0", output + assert_match "Host 1.1.1.2: --publish 80:80 --publish 8443:443 --label=foo=bar basecamp/kamal-proxy:v1.0.0", output end end diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index e42415b1..1ebd4009 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "run" do assert_equal \ - "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", + "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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", new_command.run.join(" ") end @@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase @config.delete(:proxy) assert_equal \ - "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", + "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_MINIMUM_VERSION}\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", new_command.run.join(" ") end @@ -101,7 +101,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "version" do assert_equal \ - "docker inspect kamal-proxy --format '{{.Config.Image}}' | cut -d: -f2", + "docker inspect kamal-proxy --format '{{.Config.Image}}' | awk -F: '{print $NF}'", new_command.version.join(" ") end @@ -111,10 +111,22 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.ensure_proxy_directory.join(" ") end - test "get_boot_options" do + test "read_boot_options" do assert_equal \ "cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"", - new_command.get_boot_options.join(" ") + new_command.read_boot_options.join(" ") + end + + test "read_image" do + assert_equal \ + "cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"", + new_command.read_image.join(" ") + end + + test "read_image_version" do + assert_equal \ + "cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::PROXY_MINIMUM_VERSION}\"", + new_command.read_image_version.join(" ") end test "reset_boot_options" do @@ -123,6 +135,18 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.reset_boot_options.join(" ") end + test "reset_image" do + assert_equal \ + "rm .kamal/proxy/image", + new_command.reset_image.join(" ") + end + + test "reset_image_version" do + assert_equal \ + "rm .kamal/proxy/image_version", + new_command.reset_image_version.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/integration/docker/deployer/app/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app/.kamal/hooks/pre-deploy index 32fa04c6..eff40875 100755 --- a/test/integration/docker/deployer/app/.kamal/hooks/pre-deploy +++ b/test/integration/docker/deployer/app/.kamal/hooks/pre-deploy @@ -1,3 +1,6 @@ #!/bin/sh +set -e + +kamal proxy boot_config set --registry registry:4443 echo "Deployed!" mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-deploy diff --git a/test/integration/docker/deployer/app_with_proxied_accessory/config/deploy.yml b/test/integration/docker/deployer/app_with_proxied_accessory/config/deploy.yml index bdb547ae..ae4e217b 100644 --- a/test/integration/docker/deployer/app_with_proxied_accessory/config/deploy.yml +++ b/test/integration/docker/deployer/app_with_proxied_accessory/config/deploy.yml @@ -41,4 +41,4 @@ accessories: interval: 1 timeout: 1 path: "/" - +drain_timeout: 2 diff --git a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-deploy index 32fa04c6..eff40875 100755 --- a/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-deploy +++ b/test/integration/docker/deployer/app_with_roles/.kamal/hooks/pre-deploy @@ -1,3 +1,6 @@ #!/bin/sh +set -e + +kamal proxy boot_config set --registry registry:4443 echo "Deployed!" mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-deploy diff --git a/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy index b2d8b112..5d7b7372 100755 --- a/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy +++ b/test/integration/docker/deployer/app_with_traefik/.kamal/hooks/pre-deploy @@ -1,4 +1,7 @@ -kamal proxy boot_config set --publish false \ +set -e + +kamal proxy boot_config set --registry registry:4443 \ + --publish false \ --docker_options label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http \ label=traefik.http.routers.kamal_proxy.rule=PathPrefix\(\`/\`\) \ sysctl=net.ipv4.ip_local_port_range=\"10000\ 60999\" diff --git a/test/integration/docker/deployer/setup.sh b/test/integration/docker/deployer/setup.sh index 4867519e..24f39d7f 100755 --- a/test/integration/docker/deployer/setup.sh +++ b/test/integration/docker/deployer/setup.sh @@ -20,6 +20,7 @@ push_image_to_registry_4443() { install_kamal push_image_to_registry_4443 nginx 1-alpine-slim push_image_to_registry_4443 busybox 1.36.0 +push_image_to_registry_4443 basecamp/kamal-proxy v0.8.7 # .ssh is on a shared volume that persists between runs. Clean it up as the # churn of temporary vm IPs can eventually create conflicts. diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index 39ef9bc7..88c262c9 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -12,11 +12,11 @@ class IntegrationTest < ActiveSupport::TestCase teardown do unless passed? - [ :deployer, :vm1, :vm2, :shared, :load_balancer, :registry ].each do |container| - puts - puts "Logs for #{container}:" - docker_compose :logs, container - end + # [ :deployer, :vm1, :vm2, :shared, :load_balancer, :registry ].each do |container| + # puts + # puts "Logs for #{container}:" + # docker_compose :logs, container + # end end docker_compose "down -t 1" end diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index 39cacb94..5a53a08e 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -6,6 +6,8 @@ class ProxyTest < IntegrationTest end test "boot, reboot, stop, start, restart, logs, remove" do + kamal :proxy, :boot_config, :set, "--registry", "registry:4443" + kamal :proxy, :boot assert_proxy_running @@ -46,7 +48,7 @@ class ProxyTest < IntegrationTest logs = kamal :proxy, :logs, capture: true assert_match /No previous state to restore/, logs - kamal :proxy, :boot_config, :set, "--docker-options='sysctl net.ipv4.ip_local_port_range=\"10000 60999\"'" + kamal :proxy, :boot_config, :set, "--registry", "registry:4443", "--docker-options='sysctl net.ipv4.ip_local_port_range=\"10000 60999\"'" assert_docker_options_in_file kamal :proxy, :reboot, "-y" From bd81632439d9024daf2f3f1f603f06d2f8519a27 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 16 Apr 2025 16:14:11 +0100 Subject: [PATCH 3/5] Set DEBUG for integration test output --- test/cli/build_test.rb | 2 +- test/integration/integration_test.rb | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 76db2924..8690e00b 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -321,7 +321,7 @@ class CliBuildTest < CliTestCase private def run_command(*command, fixture: :with_accessories) - stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } + stdouted { stderred { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } } end def stub_dependency_checks diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index 88c262c9..e6e5520d 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -11,12 +11,12 @@ class IntegrationTest < ActiveSupport::TestCase end teardown do - unless passed? - # [ :deployer, :vm1, :vm2, :shared, :load_balancer, :registry ].each do |container| - # puts - # puts "Logs for #{container}:" - # docker_compose :logs, container - # end + if !passed? && ENV["DEBUG"] + [ :deployer, :vm1, :vm2, :shared, :load_balancer, :registry ].each do |container| + puts + puts "Logs for #{container}:" + docker_compose :logs, container + end end docker_compose "down -t 1" end @@ -25,8 +25,8 @@ class IntegrationTest < ActiveSupport::TestCase def docker_compose(*commands, capture: false, raise_on_error: true) command = "TEST_ID=#{ENV["TEST_ID"]} docker compose #{commands.join(" ")}" succeeded = false - if capture - result = stdouted { succeeded = system("cd test/integration && #{command}") } + if capture || !ENV["DEBUG"] + result = stdouted { stderred { succeeded = system("cd test/integration && #{command}") } } else succeeded = system("cd test/integration && #{command}") end From dd9048e09ce55414c4b21d35782ec67dda1fd9af Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 17 Apr 2025 08:20:25 +0100 Subject: [PATCH 4/5] Allow version 'next' --- lib/kamal/cli/proxy.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 9ce8373e..f56f8ad9 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -1,4 +1,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base + delegate :older_version?, to: Kamal::Utils + desc "boot", "Boot proxy on servers" def boot with_lock do @@ -13,7 +15,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base version = capture_with_info(*KAMAL.proxy.version).strip.presence - if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION) + if version && version != "next" && older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION) raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" end execute *KAMAL.proxy.start_or_run From 8ceeda6ac9496290d86569ebd5e60ef605fa650f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 17 Apr 2025 08:26:10 +0100 Subject: [PATCH 5/5] Extract proxy_default_boot_options --- lib/kamal/cli/proxy.rb | 16 +++++++--------- lib/kamal/configuration.rb | 7 +++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index f56f8ad9..05c43d60 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -1,6 +1,4 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base - delegate :older_version?, to: Kamal::Utils - desc "boot", "Boot proxy on servers" def boot with_lock do @@ -15,7 +13,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base version = capture_with_info(*KAMAL.proxy.version).strip.presence - if version && version != "next" && older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION) + if version && version != "next" && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION) raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" end execute *KAMAL.proxy.start_or_run @@ -42,17 +40,17 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base *options[:docker_options].map { |option| "--#{option}" } ] - default_boot_options = [ - *(KAMAL.config.proxy_publish_args(Kamal::Configuration::PROXY_HTTP_PORT, Kamal::Configuration::PROXY_HTTPS_PORT, nil)), - *(KAMAL.config.proxy_logging_args(Kamal::Configuration::PROXY_LOG_MAX_SIZE)), - ] + image = [ + options[:registry].presence, + options[:repository].presence || KAMAL.config.proxy_repository_name, + KAMAL.config.proxy_image_name + ].compact.join("/") - image = [ options[:registry].presence, options[:repository] || KAMAL.config.proxy_repository_name, KAMAL.config.proxy_image_name ].compact.join("/") image_version = options[:image_version] on(KAMAL.proxy_hosts) do |host| execute(*KAMAL.proxy.ensure_proxy_directory) - if boot_options != default_boot_options + if boot_options != KAMAL.config.proxy_default_boot_options upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file else execute *KAMAL.proxy.reset_boot_options, raise_on_non_zero_exit: false diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 1e9cd667..6e9b7b70 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -257,6 +257,13 @@ class Kamal::Configuration argumentize "--log-opt", "max-size=#{max_size}" if max_size.present? end + def proxy_default_boot_options + [ + *(KAMAL.config.proxy_publish_args(Kamal::Configuration::PROXY_HTTP_PORT, Kamal::Configuration::PROXY_HTTPS_PORT, nil)), + *(KAMAL.config.proxy_logging_args(Kamal::Configuration::PROXY_LOG_MAX_SIZE)) + ] + end + def proxy_options_default [ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ] end