From 7627f74e453278e47a62eaea941feeee58f8234b Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Wed, 8 Jan 2025 17:13:10 +0100 Subject: [PATCH 01/11] Handle parentheses in variables in commands --- lib/kamal/secrets/dotenv/inline_command_substitution.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/kamal/secrets/dotenv/inline_command_substitution.rb b/lib/kamal/secrets/dotenv/inline_command_substitution.rb index c9ef9879..16498ffb 100644 --- a/lib/kamal/secrets/dotenv/inline_command_substitution.rb +++ b/lib/kamal/secrets/dotenv/inline_command_substitution.rb @@ -4,7 +4,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution ::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub } end - def call(value, _env, overwrite: false) + def call(value, env, overwrite: false) # Process interpolated shell commands value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*| # Eliminate opening and closing parentheses @@ -14,6 +14,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution # Command is escaped, don't replace it. $LAST_MATCH_INFO[0][1..] else + command = ::Dotenv::Substitutions::Variable.call(command, env) if command =~ /\A\s*kamal\s*secrets\s+/ # Inline the command inline_secrets_command(command) From 5145289625ada55e420728a89de9168218252d99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 04:53:20 +0000 Subject: [PATCH 02/11] Bump nokogiri in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [nokogiri](https://github.com/sparklemotion/nokogiri). Updates `nokogiri` from 1.18.4 to 1.18.8 - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.4...v1.18.8) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.18.8 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 17b354ee..b85c77fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,15 +82,15 @@ GEM net-sftp (4.0.0) net-ssh (>= 5.0.0, < 8.0.0) net-ssh (7.3.0) - nokogiri (1.18.4-aarch64-linux-musl) + nokogiri (1.18.8-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.4-arm64-darwin) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.4-x86_64-darwin) + nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.4-x86_64-linux-gnu) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.4-x86_64-linux-musl) + nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.1) parallel (1.26.3) From 63f65d60c6a32cfef07fb8099bd190d38f0bbd84 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 22 Apr 2025 08:26:51 +0100 Subject: [PATCH 03/11] Escape the audit line Makes it compatible with zsh. --- lib/kamal/commands/auditor.rb | 16 +++++++++++----- test/commands/auditor_test.rb | 12 ++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/kamal/commands/auditor.rb b/lib/kamal/commands/auditor.rb index 9846d8e2..e4981650 100644 --- a/lib/kamal/commands/auditor.rb +++ b/lib/kamal/commands/auditor.rb @@ -1,5 +1,6 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base attr_reader :details + delegate :escape_shell_value, to: Kamal::Utils def initialize(config, **details) super(config) @@ -9,11 +10,8 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base # Runs remotely def record(line, **details) combine \ - [ :mkdir, "-p", config.run_directory ], - append( - [ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ], - audit_log_file - ) + make_run_directory, + append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file) end def reveal @@ -30,4 +28,12 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base def audit_tags(**details) tags(**self.details, **details) end + + def make_run_directory + [ :mkdir, "-p", config.run_directory ] + end + + def audit_line(line, **details) + "#{audit_tags(**details).except(:version, :service_version, :service)} #{line}" + end end diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb index 2abc8d81..d1cab610 100644 --- a/test/commands/auditor_test.rb +++ b/test/commands/auditor_test.rb @@ -20,8 +20,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase assert_equal [ :mkdir, "-p", ".kamal", "&&", :echo, - "[#{@recorded_at}] [#{@performer}]", - "app removed container", + "\"[#{@recorded_at}] [#{@performer}] app removed container\"", ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container") end @@ -31,8 +30,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase assert_equal [ :mkdir, "-p", ".kamal", "&&", :echo, - "[#{@recorded_at}] [#{@performer}] [staging]", - "app removed container", + "\"[#{@recorded_at}] [#{@performer}] [staging] app removed container\"", ">>", ".kamal/app-staging-audit.log" ], auditor.record("app removed container") end @@ -43,8 +41,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase assert_equal [ :mkdir, "-p", ".kamal", "&&", :echo, - "[#{@recorded_at}] [#{@performer}] [web]", - "app removed container", + "\"[#{@recorded_at}] [#{@performer}] [web] app removed container\"", ">>", ".kamal/app-audit.log" ], auditor.record("app removed container") end @@ -54,8 +51,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase assert_equal [ :mkdir, "-p", ".kamal", "&&", :echo, - "[#{@recorded_at}] [#{@performer}] [value]", - "app removed container", + "\"[#{@recorded_at}] [#{@performer}] [value] app removed container\"", ">>", ".kamal/app-audit.log" ], @auditor.record("app removed container", detail: "value") end From 04568dea2f4f252e43451497dace72cbea2dae9a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 22 Apr 2025 09:00:22 +0100 Subject: [PATCH 04/11] Inherit locks We'll set the KAMAL_LOCK environment when calling run hooks. If set to true we have the lock and the hook will not need to acquire it again if it runs kamal commands. Fixes: https://github.com/basecamp/kamal/issues/1517 --- lib/kamal/cli/base.rb | 8 +++++++- lib/kamal/commander.rb | 2 +- test/cli/main_test.rb | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 7e5dd77b..9005e1c7 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -133,7 +133,13 @@ module Kamal::Cli def run_hook(hook, **extra_details) if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook) - details = { hosts: KAMAL.hosts.join(","), roles: KAMAL.specific_roles&.join(","), command: command, subcommand: subcommand }.compact + details = { + hosts: KAMAL.hosts.join(","), + roles: KAMAL.specific_roles&.join(","), + lock: KAMAL.holding_lock?.to_s, + command: command, + subcommand: subcommand + }.compact say "Running the #{hook} hook...", :magenta with_env KAMAL.hook.env(**details, **extra_details) do diff --git a/lib/kamal/commander.rb b/lib/kamal/commander.rb index 8a4356ed..0882311d 100644 --- a/lib/kamal/commander.rb +++ b/lib/kamal/commander.rb @@ -13,7 +13,7 @@ class Kamal::Commander def reset self.verbosity = :info - self.holding_lock = false + self.holding_lock = ENV["KAMAL_LOCK"] == "true" self.connected = false @specifics = @specific_roles = @specific_hosts = nil @config = @config_kwargs = nil diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index d37c9352..735fbd00 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -43,7 +43,7 @@ class CliMainTest < CliTestCase 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:build:deliver", [], invoke_options) + Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options) Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy: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) @@ -53,7 +53,7 @@ class CliMainTest < CliTestCase run_command("deploy", "--verbose").tap do |output| assert_hook_ran "pre-connect", output - assert_match /Build and push app image/, output + assert_match /Build and push app image/, output assert_hook_ran "pre-deploy", output assert_match /Ensure kamal-proxy is running/, output assert_match /Detect stale containers/, output @@ -116,6 +116,32 @@ class CliMainTest < CliTestCase end end + test "deploy when inheriting lock" do + Thread.report_on_exception = false + + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => 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:proxy: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) + + with_kamal_lock_env do + KAMAL.reset + run_command("deploy").tap do |output| + assert_no_match /Acquiring the deploy lock/, output + assert_match /Build and push app image/, output + assert_match /Ensure kamal-proxy is running/, output + assert_match /Detect stale containers/, output + assert_match /Prune old containers and images/, output + assert_no_match /Releasing the deploy lock/, output + end + end + end + test "deploy error when locking" do Thread.report_on_exception = false @@ -562,4 +588,11 @@ class CliMainTest < CliTestCase def assert_file(file, content) assert_match content, File.read(file) end + + def with_kamal_lock_env + ENV["KAMAL_LOCK"] = "true" + yield + ensure + ENV.delete("KAMAL_LOCK") + end end From d26b3f176845b761f7bf49be1d39fe575a7b5f14 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 22 Apr 2025 15:18:54 +0100 Subject: [PATCH 05/11] Create the .kamal/proxy/apps-config directory Manually create it to avoid ownership issues when docker creates it for you. --- lib/kamal/cli/proxy.rb | 2 ++ lib/kamal/commands/proxy.rb | 4 ++++ test/cli/proxy_test.rb | 3 +++ test/commands/proxy_test.rb | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 1ed17740..4baf9419 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -16,6 +16,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base if version && 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.ensure_apps_config_directory execute *KAMAL.proxy.start_or_run end end @@ -101,6 +102,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base "Stopping and removing kamal-proxy on #{host}, if running..." execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false execute *KAMAL.proxy.remove_container + execute *KAMAL.proxy.ensure_apps_config_directory execute *KAMAL.proxy.run diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 658252de..d3eb6c31 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -65,6 +65,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base remove_directory config.proxy_directory end + def ensure_apps_config_directory + make_directory config.proxy_apps_directory + end + def boot_config [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)}" ] end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 2ec4c726..604676fe 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -4,6 +4,7 @@ class CliProxyTest < CliTestCase test "boot" do run_command("boot").tap do |output| assert_match "docker login", output + assert_match "mkdir -p .kamal/proxy/apps-config", 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 @@ -56,11 +57,13 @@ 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 "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_MINIMUM_VERSION}\") | 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_MINIMUM_VERSION}\") | 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 diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 21863fc6..94fe9c66 100644 --- a/test/commands/proxy_test.rb +++ b/test/commands/proxy_test.rb @@ -147,6 +147,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.reset_image_version.join(" ") end + test "ensure_apps_config_directory" do + assert_equal \ + "mkdir -p .kamal/proxy/apps-config", + new_command.ensure_apps_config_directory.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) From 128294672db9b2566595912bc299ae18403895f8 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 23 Apr 2025 16:01:04 +0100 Subject: [PATCH 06/11] Extract Kamal::Configuration::Proxy::Boot This is for boot time configuration for the kamal proxy. Config in here doesn't not belong in Kamal::Configuration::Proxy which is for deploy time configuration for the app itself. Kamal apps don't contain boot time config, because multiple apps can share a proxy and the config could conflict. --- lib/kamal/cli/app/error_pages.rb | 2 +- lib/kamal/cli/proxy.rb | 30 ++++--- lib/kamal/commands/accessory.rb | 1 - lib/kamal/commands/accessory/proxy.rb | 2 +- lib/kamal/commands/app/error_pages.rb | 4 +- lib/kamal/commands/app/proxy.rb | 4 +- lib/kamal/commands/proxy.rb | 22 ++--- lib/kamal/configuration.rb | 115 +------------------------ lib/kamal/configuration/proxy.rb | 2 +- lib/kamal/configuration/proxy/boot.rb | 117 ++++++++++++++++++++++++++ test/cli/proxy_test.rb | 20 ++--- test/commands/proxy_test.rb | 6 +- test/configuration/proxy/boot_test.rb | 29 +++++++ test/configuration_test.rb | 9 -- test/integration/main_test.rb | 2 +- 15 files changed, 196 insertions(+), 169 deletions(-) create mode 100644 lib/kamal/configuration/proxy/boot.rb create mode 100644 test/configuration/proxy/boot_test.rb diff --git a/lib/kamal/cli/app/error_pages.rb b/lib/kamal/cli/app/error_pages.rb index 664f7eff..d15ddf7a 100644 --- a/lib/kamal/cli/app/error_pages.rb +++ b/lib/kamal/cli/app/error_pages.rb @@ -13,7 +13,7 @@ class Kamal::Cli::App::ErrorPages if KAMAL.config.error_pages_path with_error_pages_tmpdir do |local_error_pages_dir| execute *KAMAL.app.create_error_pages_directory - upload! local_error_pages_dir, KAMAL.config.proxy_error_pages_directory, mode: "0700", recursive: true + upload! local_error_pages_dir, KAMAL.config.proxy_boot.error_pages_directory, mode: "0700", recursive: true end end end diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 4baf9419..cd4e45d9 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -13,8 +13,8 @@ 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) - raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" + if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) + raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}" end execute *KAMAL.proxy.ensure_apps_config_directory execute *KAMAL.proxy.start_or_run @@ -25,46 +25,48 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base desc "boot_config ", "Manage kamal-proxy boot configuration" option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host" option :publish_host_ip, type: :string, repeatable: true, default: nil, desc: "Host IP address to bind HTTP/HTTPS traffic to. Defaults to all interfaces" - 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 :http_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTP_PORT, desc: "HTTP port to publish on the host" + option :https_port, type: :numeric, default: Kamal::Configuration::Proxy::Boot::DEFAULT_HTTPS_PORT, desc: "HTTPS port to publish on the host" + option :log_max_size, type: :string, default: Kamal::Configuration::Proxy::Boot::DEFAULT_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) + proxy_boot_config = KAMAL.config.proxy_boot + case subcommand when "set" boot_options = [ - *(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]), - *(KAMAL.config.proxy_logging_args(options[:log_max_size])), + *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]), + *(proxy_boot_config.logging_args(options[:log_max_size])), *options[:docker_options].map { |option| "--#{option}" } ] image = [ options[:registry].presence, - options[:repository].presence || KAMAL.config.proxy_repository_name, - KAMAL.config.proxy_image_name + options[:repository].presence || proxy_boot_config.repository_name, + proxy_boot_config.image_name ].compact.join("/") image_version = options[:image_version] on(KAMAL.proxy_hosts) do |host| execute(*KAMAL.proxy.ensure_proxy_directory) - if boot_options != KAMAL.config.proxy_default_boot_options - upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file + if boot_options != proxy_boot_config.default_boot_options + upload! StringIO.new(boot_options.join(" ")), proxy_boot_config.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 + if image != proxy_boot_config.image_default + upload! StringIO.new(image), proxy_boot_config.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 + upload! StringIO.new(image_version), proxy_boot_config.image_version_file else execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false end diff --git a/lib/kamal/commands/accessory.rb b/lib/kamal/commands/accessory.rb index 60279dfc..4a76d6f6 100644 --- a/lib/kamal/commands/accessory.rb +++ b/lib/kamal/commands/accessory.rb @@ -6,7 +6,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args, :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry, to: :accessory_config - delegate :proxy_container_name, to: :config def initialize(config, name:) super(config) diff --git a/lib/kamal/commands/accessory/proxy.rb b/lib/kamal/commands/accessory/proxy.rb index 195a321b..833088da 100644 --- a/lib/kamal/commands/accessory/proxy.rb +++ b/lib/kamal/commands/accessory/proxy.rb @@ -1,5 +1,5 @@ module Kamal::Commands::Accessory::Proxy - delegate :proxy_container_name, to: :config + delegate :container_name, to: :"config.proxy_boot", prefix: :proxy def deploy(target:) proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target) diff --git a/lib/kamal/commands/app/error_pages.rb b/lib/kamal/commands/app/error_pages.rb index d51dba3f..3a21bdaf 100644 --- a/lib/kamal/commands/app/error_pages.rb +++ b/lib/kamal/commands/app/error_pages.rb @@ -1,9 +1,9 @@ module Kamal::Commands::App::ErrorPages def create_error_pages_directory - make_directory(config.proxy_error_pages_directory) + make_directory(config.proxy_boot.error_pages_directory) end def clean_up_error_pages - [ :find, config.proxy_error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ] + [ :find, config.proxy_boot.error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ] end end diff --git a/lib/kamal/commands/app/proxy.rb b/lib/kamal/commands/app/proxy.rb index 56d0ccea..6d015a0b 100644 --- a/lib/kamal/commands/app/proxy.rb +++ b/lib/kamal/commands/app/proxy.rb @@ -1,5 +1,5 @@ module Kamal::Commands::App::Proxy - delegate :proxy_container_name, to: :config + delegate :container_name, to: :"config.proxy_boot", prefix: :proxy def deploy(target:) proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target) @@ -18,7 +18,7 @@ module Kamal::Commands::App::Proxy end def remove_proxy_app_directory - remove_directory config.proxy_app_directory + remove_directory config.proxy_boot.app_directory end private diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index d3eb6c31..2193fff7 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -58,15 +58,15 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def ensure_proxy_directory - make_directory config.proxy_directory + make_directory config.proxy_boot.host_directory end def remove_proxy_directory - remove_directory config.proxy_directory + remove_directory config.proxy_boot.host_directory end def ensure_apps_config_directory - make_directory config.proxy_apps_directory + make_directory config.proxy_boot.apps_directory end def boot_config @@ -74,32 +74,32 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def read_boot_options - read_file(config.proxy_options_file, default: config.proxy_options_default.join(" ")) + read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" ")) end def read_image - read_file(config.proxy_image_file, default: config.proxy_image_default) + read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default) end def read_image_version - read_file(config.proxy_image_version_file, default: Kamal::Configuration::PROXY_MINIMUM_VERSION) + read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) end def reset_boot_options - remove_file config.proxy_options_file + remove_file config.proxy_boot.options_file end def reset_image - remove_file config.proxy_image_file + remove_file config.proxy_boot.image_file end def reset_image_version - remove_file config.proxy_image_version_file + remove_file config.proxy_boot.image_version_file end private def container_name - config.proxy_container_name + config.proxy_boot.container_name end def read_file(file, default: nil) @@ -114,6 +114,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base "--detach", "--restart", "unless-stopped", "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy", - *config.proxy_apps_volume.docker_args + *config.proxy_boot.apps_volume.docker_args end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index 326e1f71..26450170 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -10,15 +10,10 @@ class Kamal::Configuration delegate :argumentize, :optionize, to: Kamal::Utils attr_reader :destination, :raw_config, :secrets - attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry + attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry include Validation - PROXY_MINIMUM_VERSION = "v0.8.7" - PROXY_HTTP_PORT = 80 - PROXY_HTTPS_PORT = 443 - PROXY_LOG_MAX_SIZE = "10m" - class << self def create_from(config_file:, destination: nil, version: nil) ENV["KAMAL_DESTINATION"] = destination @@ -69,6 +64,7 @@ class Kamal::Configuration @logging = Logging.new(logging_config: @raw_config.logging) @proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {}) + @proxy_boot = Proxy::Boot.new(config: self) @ssh = Ssh.new(config: self) @sshkit = Sshkit.new(config: self) @@ -257,95 +253,6 @@ class Kamal::Configuration env_tags.detect { |t| t.name == name.to_s } end - def proxy_publish_args(http_port, https_port, bind_ips = nil) - ensure_valid_bind_ips(bind_ips) - - (bind_ips || [ nil ]).map do |bind_ip| - bind_ip = format_bind_ip(bind_ip) - publish_http = [ bind_ip, http_port, PROXY_HTTP_PORT ].compact.join(":") - publish_https = [ bind_ip, https_port, PROXY_HTTPS_PORT ].compact.join(":") - - argumentize "--publish", [ publish_http, publish_https ] - end.join(" ") - end - - def proxy_logging_args(max_size) - 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 - - 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 - "kamal-proxy" - end - - def proxy_directory - File.join run_directory, "proxy" - end - - def proxy_options_file - 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 proxy_apps_directory - File.join proxy_directory, "apps-config" - end - - def proxy_apps_container_directory - "/home/kamal-proxy/.apps-config" - end - - def proxy_apps_volume - Volume.new \ - host_path: proxy_apps_directory, - container_path: proxy_apps_container_directory - end - - def proxy_app_directory - File.join proxy_apps_directory, service_and_destination - end - - def proxy_app_container_directory - File.join proxy_apps_container_directory, service_and_destination - end - - def proxy_error_pages_directory - File.join proxy_app_directory, "error_pages" - end - - def proxy_error_pages_container_directory - File.join proxy_app_container_directory, "error_pages" - end - def to_h { roles: role_names, @@ -416,15 +323,6 @@ class Kamal::Configuration true end - def ensure_valid_bind_ips(bind_ips) - bind_ips.present? && bind_ips.each do |ip| - next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex - raise ArgumentError, "Invalid publish IP address: #{ip}" - end - - true - end - def ensure_retain_containers_valid raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1 @@ -456,15 +354,6 @@ class Kamal::Configuration true end - def format_bind_ip(ip) - # Ensure IPv6 address inside square brackets - e.g. [::1] - if ip =~ Resolv::IPv6::Regex && ip !~ /\[.*\]/ - "[#{ip}]" - else - ip - end - end - def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort end diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 19bc25f7..b5afbaae 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -74,6 +74,6 @@ class Kamal::Configuration::Proxy end def error_pages - File.join config.proxy_error_pages_container_directory, config.version if config.error_pages_path + File.join config.proxy_boot.error_pages_container_directory, config.version if config.error_pages_path end end diff --git a/lib/kamal/configuration/proxy/boot.rb b/lib/kamal/configuration/proxy/boot.rb new file mode 100644 index 00000000..19adfa91 --- /dev/null +++ b/lib/kamal/configuration/proxy/boot.rb @@ -0,0 +1,117 @@ +class Kamal::Configuration::Proxy::Boot + MINIMUM_VERSION = "v0.8.7" + DEFAULT_HTTP_PORT = 80 + DEFAULT_HTTPS_PORT = 443 + DEFAULT_LOG_MAX_SIZE = "10m" + + attr_reader :config + delegate :argumentize, :optionize, to: Kamal::Utils + + def initialize(config:) + @config = config + end + + def publish_args(http_port, https_port, bind_ips = nil) + ensure_valid_bind_ips(bind_ips) + + (bind_ips || [ nil ]).map do |bind_ip| + bind_ip = format_bind_ip(bind_ip) + publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":") + publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":") + + argumentize "--publish", [ publish_http, publish_https ] + end.join(" ") + end + + def logging_args(max_size) + argumentize "--log-opt", "max-size=#{max_size}" if max_size.present? + end + + def default_boot_options + [ + *(publish_args(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, nil)), + *(logging_args(DEFAULT_LOG_MAX_SIZE)) + ] + end + + def repository_name + "basecamp" + end + + def image_name + "kamal-proxy" + end + + def image_default + "#{repository_name}/#{image_name}" + end + + def container_name + "kamal-proxy" + end + + def host_directory + File.join config.run_directory, "proxy" + end + + def options_file + File.join host_directory, "options" + end + + def image_file + File.join host_directory, "image" + end + + def image_version_file + File.join host_directory, "image_version" + end + + def apps_directory + File.join host_directory, "apps-config" + end + + def apps_container_directory + "/home/kamal-proxy/.apps-config" + end + + def apps_volume + Kamal::Configuration::Volume.new \ + host_path: apps_directory, + container_path: apps_container_directory + end + + def app_directory + File.join apps_directory, config.service_and_destination + end + + def app_container_directory + File.join apps_container_directory, config.service_and_destination + end + + def error_pages_directory + File.join app_directory, "error_pages" + end + + def error_pages_container_directory + File.join app_container_directory, "error_pages" + end + + private + def ensure_valid_bind_ips(bind_ips) + bind_ips.present? && bind_ips.each do |ip| + next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex + raise ArgumentError, "Invalid publish IP address: #{ip}" + end + + true + end + + def format_bind_ip(ip) + # Ensure IPv6 address inside square brackets - e.g. [::1] + if ip =~ Resolv::IPv6::Regex && ip !~ /\[.*\]/ + "[#{ip}]" + else + ip + end + end +end diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 604676fe..428fc515 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -5,7 +5,7 @@ class CliProxyTest < CliTestCase run_command("boot").tap do |output| assert_match "docker login", output assert_match "mkdir -p .kamal/proxy/apps-config", 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 + 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}\") | 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 @@ -19,11 +19,11 @@ class CliProxyTest < CliTestCase 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\") $(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 "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}\") | 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 - assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}" + assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}" ensure Thread.report_on_exception = false end @@ -32,12 +32,12 @@ class CliProxyTest < CliTestCase Thread.report_on_exception = false SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") - .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) + .returns(Kamal::Configuration::Proxy::Boot::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\") $(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 "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::Boot::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 @@ -58,13 +58,13 @@ class CliProxyTest < CliTestCase 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_MINIMUM_VERSION}\") | 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 "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}\") | 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_MINIMUM_VERSION}\") | 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 "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}\") | 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 @@ -183,7 +183,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") - .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) + .returns(Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") @@ -199,7 +199,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\") $(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 "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::Boot::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 @@ -222,7 +222,7 @@ class CliProxyTest < CliTestCase SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :awk, "-F:", "'{print $NF}'") - .returns(Kamal::Configuration::PROXY_MINIMUM_VERSION) + .returns(Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'") diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index 94fe9c66..e141725a 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\") $(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 --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", + "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}\") | 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", 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\") $(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 --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config", + "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}\") | 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", new_command.run.join(" ") end @@ -125,7 +125,7 @@ class CommandsProxyTest < ActiveSupport::TestCase test "read_image_version" do assert_equal \ - "cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::PROXY_MINIMUM_VERSION}\"", + "cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\"", new_command.read_image_version.join(" ") end diff --git a/test/configuration/proxy/boot_test.rb b/test/configuration/proxy/boot_test.rb new file mode 100644 index 00000000..90dee1b7 --- /dev/null +++ b/test/configuration/proxy/boot_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class ConfigurationProxyBootTest < ActiveSupport::TestCase + setup do + ENV["RAILS_MASTER_KEY"] = "456" + ENV["VERSION"] = "missing" + + @deploy = { + service: "app", image: "dhh/app", + registry: { "username" => "dhh", "password" => "secret" }, + builder: { "arch" => "amd64" }, + env: { "REDIS_URL" => "redis://x/y" }, + servers: [ "1.1.1.1", "1.1.1.2" ], + volumes: [ "/local/path:/container/path" ] + } + + @config = Kamal::Configuration.new(@deploy) + @proxy_boot_config = @config.proxy_boot + end + + test "proxy directories" do + assert_equal ".kamal/proxy/apps-config", @proxy_boot_config.apps_directory + assert_equal "/home/kamal-proxy/.apps-config", @proxy_boot_config.apps_container_directory + assert_equal ".kamal/proxy/apps-config/app", @proxy_boot_config.app_directory + assert_equal "/home/kamal-proxy/.apps-config/app", @proxy_boot_config.app_container_directory + assert_equal ".kamal/proxy/apps-config/app/error_pages", @proxy_boot_config.error_pages_directory + assert_equal "/home/kamal-proxy/.apps-config/app/error_pages", @proxy_boot_config.error_pages_container_directory + end +end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 571ed0a7..73756c88 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -410,13 +410,4 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message end - - test "proxy directories" do - assert_equal ".kamal/proxy/apps-config", @config.proxy_apps_directory - assert_equal "/home/kamal-proxy/.apps-config", @config.proxy_apps_container_directory - assert_equal ".kamal/proxy/apps-config/app", @config.proxy_app_directory - assert_equal "/home/kamal-proxy/.apps-config/app", @config.proxy_app_container_directory - assert_equal ".kamal/proxy/apps-config/app/error_pages", @config.proxy_error_pages_directory - assert_equal "/home/kamal-proxy/.apps-config/app/error_pages", @config.proxy_error_pages_container_directory - end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 3b56fbb3..604bd78c 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -32,7 +32,7 @@ class MainTest < IntegrationTest assert_match /Proxy Host: vm2/, details assert_match /App Host: vm1/, details assert_match /App Host: vm2/, details - assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}/, details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true From b1c5c5092f8c1256ece44b52106e4ecead46d7cd Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 24 Apr 2025 08:17:02 +0100 Subject: [PATCH 07/11] Fix polynomial regexp issue --- lib/kamal/configuration/proxy/boot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration/proxy/boot.rb b/lib/kamal/configuration/proxy/boot.rb index 19adfa91..668c0cea 100644 --- a/lib/kamal/configuration/proxy/boot.rb +++ b/lib/kamal/configuration/proxy/boot.rb @@ -108,7 +108,7 @@ class Kamal::Configuration::Proxy::Boot def format_bind_ip(ip) # Ensure IPv6 address inside square brackets - e.g. [::1] - if ip =~ Resolv::IPv6::Regex && ip !~ /\[.*\]/ + if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/ "[#{ip}]" else ip From 2f7feaf59d9a9b9d340ac25c1fcf66daa81ea890 Mon Sep 17 00:00:00 2001 From: Ryan Ahearn Date: Sat, 26 Apr 2025 12:17:59 -0400 Subject: [PATCH 08/11] Update name of KAMAL_ROLES in sample hooks files --- lib/kamal/cli/templates/sample_hooks/post-deploy.sample | 2 +- lib/kamal/cli/templates/sample_hooks/pre-build.sample | 2 +- lib/kamal/cli/templates/sample_hooks/pre-connect.sample | 2 +- lib/kamal/cli/templates/sample_hooks/pre-deploy.sample | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/kamal/cli/templates/sample_hooks/post-deploy.sample b/lib/kamal/cli/templates/sample_hooks/post-deploy.sample index 75efafc1..fd364c2a 100755 --- a/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +++ b/lib/kamal/cli/templates/sample_hooks/post-deploy.sample @@ -7,7 +7,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # KAMAL_RUNTIME diff --git a/lib/kamal/cli/templates/sample_hooks/pre-build.sample b/lib/kamal/cli/templates/sample_hooks/pre-build.sample index f87d8113..c5a55678 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-build.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-build.sample @@ -13,7 +13,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) if [ -n "$(git status --porcelain)" ]; then diff --git a/lib/kamal/cli/templates/sample_hooks/pre-connect.sample b/lib/kamal/cli/templates/sample_hooks/pre-connect.sample index 18e61d7e..77744bdc 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-connect.sample @@ -9,7 +9,7 @@ # KAMAL_PERFORMER # KAMAL_VERSION # KAMAL_HOSTS -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # KAMAL_RUNTIME diff --git a/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample b/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample index 06b86aa1..665197f1 100755 --- a/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +++ b/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample @@ -13,7 +13,7 @@ # KAMAL_HOSTS # KAMAL_COMMAND # KAMAL_SUBCOMMAND -# KAMAL_ROLE (if set) +# KAMAL_ROLES (if set) # KAMAL_DESTINATION (if set) # Only check the build status for production deployments From 1ea5d0bd8664f1e8697d745d4d6bf05dfa27df3d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 17 Apr 2025 14:44:19 +0100 Subject: [PATCH 09/11] Allow kamal-proxy run command options to be set Allow --metrics_port and --debug options to be set via the boot config. --metrics_port support will come in kamal-proxy v0.8.8, so this option doesn't work right now. This will be updated before the next Kamal release though and we can add integration tests for the metrics at that point. --- lib/kamal/cli/proxy.rb | 12 ++++++ lib/kamal/commands/proxy.rb | 10 ++++- test/cli/proxy_test.rb | 40 ++++++++++++++----- test/commands/proxy_test.rb | 16 +++++++- .../.kamal/hooks/pre-deploy | 4 ++ 5 files changed, 70 insertions(+), 12 deletions(-) create mode 100755 test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index cd4e45d9..91bcdfc1 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -31,6 +31,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 :metrics_port, type: :numeric, default: nil, desc: "Port to report prometheus metrics on" + option :debug, type: :boolean, default: false, desc: "Whether to run the proxy in debug mode" option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2" def boot_config(subcommand) proxy_boot_config = KAMAL.config.proxy_boot @@ -51,6 +53,9 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base image_version = options[:image_version] + run_command_options = { debug: options[:debug] || nil, "metrics-port": options[:metrics_port] }.compact + run_command = "kamal-proxy run #{Kamal::Utils.optionize(run_command_options).join(" ")}" if run_command_options.any? + on(KAMAL.proxy_hosts) do |host| execute(*KAMAL.proxy.ensure_proxy_directory) if boot_options != proxy_boot_config.default_boot_options @@ -70,6 +75,12 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base else execute *KAMAL.proxy.reset_image_version, raise_on_non_zero_exit: false end + + if run_command + upload! StringIO.new(run_command), KAMAL.config.proxy_run_command_file + else + execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false + end end when "get" @@ -81,6 +92,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base 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 + execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false end else raise ArgumentError, "Unknown boot_config subcommand #{subcommand}" diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index 2193fff7..b9b255f5 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -70,7 +70,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def boot_config - [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)}" ] + [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ] end def read_boot_options @@ -85,6 +85,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION) end + def read_run_command + read_file(config.proxy_run_command_file) + end + def reset_boot_options remove_file config.proxy_boot.options_file end @@ -97,6 +101,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base remove_file config.proxy_boot.image_version_file end + def reset_run_command + remove_file config.proxy_run_command_file + end + private def container_name config.proxy_boot.container_name diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index 428fc515..fea95406 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -5,7 +5,7 @@ class CliProxyTest < CliTestCase run_command("boot").tap do |output| assert_match "docker login", output assert_match "mkdir -p .kamal/proxy/apps-config", 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}\") | 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::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", output end end @@ -19,7 +19,7 @@ class CliProxyTest < CliTestCase 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\") $(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}\") | 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::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", output end end @@ -37,7 +37,7 @@ class CliProxyTest < CliTestCase 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\") $(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}\") | 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::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", output end ensure Thread.report_on_exception = false @@ -58,13 +58,13 @@ class CliProxyTest < CliTestCase 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}\") | 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 "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}\") | 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 "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 @@ -199,7 +199,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\") $(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}\") | 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::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", 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 @@ -243,7 +243,9 @@ class CliProxyTest < CliTestCase 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 "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output end + assert_match "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output + end end end @@ -254,6 +256,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -265,6 +268,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -276,6 +280,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -322,6 +327,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -333,6 +339,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -344,6 +351,7 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output end end end @@ -355,23 +363,37 @@ class CliProxyTest < CliTestCase 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 + assert_match "Running /usr/bin/env rm .kamal/proxy/run_command on #{host}", output + end + end + end + + test "boot_config set run_command" do + run_command("boot_config", "set", "--metrics_port", "9000", "--debug", "true").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 "Running /usr/bin/env rm .kamal/proxy/image_version on #{host}", output + assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command 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| + run_command("boot_config", "set", "--docker_options", "label=foo=bar", "--registry", "myreg", "--repository", "myrepo", "--image_version", "0.9.9", "--metrics_port", "9000", "--debug", "true").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 + assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command on #{host}", output end end end test "boot_config get" do SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) - .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\")") + .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 \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\")") .returns("--publish 80:80 --publish 8443:443 --label=foo=bar basecamp/kamal-proxy:v1.0.0") .twice diff --git a/test/commands/proxy_test.rb b/test/commands/proxy_test.rb index e141725a..2bb2dadb 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\") $(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}\") | 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", + "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", 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\") $(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}\") | 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", + "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", new_command.run.join(" ") end @@ -129,6 +129,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.read_image_version.join(" ") end + test "read_run_command" do + assert_equal \ + "cat .kamal/proxy/run_command 2> /dev/null || echo \"\"", + new_command.read_run_command.join(" ") + end + test "reset_boot_options" do assert_equal \ "rm .kamal/proxy/options", @@ -153,6 +159,12 @@ class CommandsProxyTest < ActiveSupport::TestCase new_command.ensure_apps_config_directory.join(" ") end + test "reset_run_command" do + assert_equal \ + "rm .kamal/proxy/run_command", + new_command.reset_run_command.join(" ") + end + private def new_command Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123")) diff --git a/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy b/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy new file mode 100755 index 00000000..af8d0b4a --- /dev/null +++ b/test/integration/docker/deployer/app_with_proxied_accessory/.kamal/hooks/pre-deploy @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +kamal proxy boot_config set --registry registry:4443 From e32ea2e2764ca88a6127d71fafc14ad65f643ace Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 17 Apr 2025 16:09:01 +0100 Subject: [PATCH 10/11] Expose the metrics port --- lib/kamal/cli/proxy.rb | 1 + test/cli/proxy_test.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index 91bcdfc1..a65caf52 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -42,6 +42,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base boot_options = [ *(proxy_boot_config.publish_args(options[:http_port], options[:https_port], options[:publish_host_ip]) if options[:publish]), *(proxy_boot_config.logging_args(options[:log_max_size])), + *("--expose=#{options[:metrics_port]}" if options[:metrics_port]), *options[:docker_options].map { |option| "--#{option}" } ] diff --git a/test/cli/proxy_test.rb b/test/cli/proxy_test.rb index fea95406..fd131984 100644 --- a/test/cli/proxy_test.rb +++ b/test/cli/proxy_test.rb @@ -372,7 +372,7 @@ class CliProxyTest < CliTestCase run_command("boot_config", "set", "--metrics_port", "9000", "--debug", "true").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 \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --expose=9000\" 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 assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command on #{host}", output @@ -383,7 +383,7 @@ class CliProxyTest < CliTestCase 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", "--metrics_port", "9000", "--debug", "true").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 \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --expose=9000 --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 assert_match "Uploading \"kamal-proxy run --debug --metrics-port \\\"9000\\\"\" to .kamal/proxy/run_command on #{host}", output From 226e7091dbe196260868459cc73758473bd7650d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 6 May 2025 12:14:58 +0100 Subject: [PATCH 11/11] Add run_command_file to proxy boot --- lib/kamal/cli/proxy.rb | 2 +- lib/kamal/commands/proxy.rb | 4 ++-- lib/kamal/configuration/proxy/boot.rb | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/kamal/cli/proxy.rb b/lib/kamal/cli/proxy.rb index a65caf52..73707572 100644 --- a/lib/kamal/cli/proxy.rb +++ b/lib/kamal/cli/proxy.rb @@ -78,7 +78,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base end if run_command - upload! StringIO.new(run_command), KAMAL.config.proxy_run_command_file + upload! StringIO.new(run_command), proxy_boot_config.run_command_file else execute *KAMAL.proxy.reset_run_command, raise_on_non_zero_exit: false end diff --git a/lib/kamal/commands/proxy.rb b/lib/kamal/commands/proxy.rb index b9b255f5..7699dde2 100644 --- a/lib/kamal/commands/proxy.rb +++ b/lib/kamal/commands/proxy.rb @@ -86,7 +86,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def read_run_command - read_file(config.proxy_run_command_file) + read_file(config.proxy_boot.run_command_file) end def reset_boot_options @@ -102,7 +102,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base end def reset_run_command - remove_file config.proxy_run_command_file + remove_file config.proxy_boot.run_command_file end private diff --git a/lib/kamal/configuration/proxy/boot.rb b/lib/kamal/configuration/proxy/boot.rb index 668c0cea..9f8c6f5c 100644 --- a/lib/kamal/configuration/proxy/boot.rb +++ b/lib/kamal/configuration/proxy/boot.rb @@ -66,6 +66,10 @@ class Kamal::Configuration::Proxy::Boot File.join host_directory, "image_version" end + def run_command_file + File.join host_directory, "run_command" + end + def apps_directory File.join host_directory, "apps-config" end