From 63f854ea18fa5dd501ac9f23e359187629a1337d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 18 Sep 2024 17:42:45 +0100 Subject: [PATCH] Add validations for host/ssl roles Roles with SSL can only have one server. Two roles with SSL can't use the same host. --- lib/kamal/configuration.rb | 16 ++++++++++++++++ lib/kamal/configuration/proxy.rb | 4 ++++ lib/kamal/configuration/role.rb | 10 ++++++++++ test/configuration_test.rb | 31 +++++++++++++++++++++++++++++++ test/integration/main_test.rb | 2 +- test/integration/proxy_test.rb | 4 ++-- 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index b28152bc..92d6b9d8 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -75,6 +75,8 @@ class Kamal::Configuration ensure_retain_containers_valid ensure_valid_service_name ensure_no_traefik_reboot_hooks + ensure_one_host_for_ssl_roles + ensure_unique_hosts_for_ssl_roles end @@ -349,6 +351,20 @@ class Kamal::Configuration true end + def ensure_one_host_for_ssl_roles + roles.each(&:ensure_one_host_for_ssl) + + true + end + + def ensure_unique_hosts_for_ssl_roles + hosts = roles.select(&:ssl?).map { |role| role.proxy.host } + duplicates = hosts.tally.filter_map { |host, count| host if count > 1 } + + raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any? + + true + end def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort diff --git a/lib/kamal/configuration/proxy.rb b/lib/kamal/configuration/proxy.rb index 551601c4..af09e2c7 100644 --- a/lib/kamal/configuration/proxy.rb +++ b/lib/kamal/configuration/proxy.rb @@ -22,6 +22,10 @@ class Kamal::Configuration::Proxy proxy_config.fetch("ssl", false) end + def host + proxy_config["host"] + end + def deploy_options { host: proxy_config["host"], diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index 76305312..708e77fc 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -75,6 +75,10 @@ class Kamal::Configuration::Role @running_proxy end + def ssl? + running_proxy? && proxy.ssl? + end + def stop_args # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait. timeout = running_proxy? ? nil : config.drain_timeout @@ -145,6 +149,12 @@ class Kamal::Configuration::Role File.join config.assets_directory, "volumes", [ name, version ].join("-") end + def ensure_one_host_for_ssl + if running_proxy? && proxy.ssl? && hosts.size > 1 + raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}" + end + end + private def initialize_specialized_proxy proxy_specializations = specializations["proxy"] diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 81dbb5ac..814aa26e 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -346,4 +346,35 @@ class ConfigurationTest < ActiveSupport::TestCase end end end + + test "proxy ssl roles with no host" do + @deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "servers/workers/proxy: Must set a host to enable automatic SSL", exception.message + end + + test "proxy ssl roles with multiple servers" do + @deploy_with_roles[:servers]["workers"]["proxy"] = { "ssl" => true, "host" => "foo.example.com" } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "SSL is only supported on a single server, found 2 servers for role workers", exception.message + end + + test "two proxy ssl roles with same host" do + @deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } } + @deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "host" => "foo.example.com" } } + + exception = assert_raises(Kamal::ConfigurationError) do + Kamal::Configuration.new(@deploy_with_roles) + end + + assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index 11e2ac94..a015c1ce 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -28,7 +28,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_MINIMUM_VERSION}/, details assert_match /registry:4443\/app:#{first_version}/, details audit = kamal :audit, capture: true diff --git a/test/integration/proxy_test.rb b/test/integration/proxy_test.rb index 2888ebfd..0eb65aec 100644 --- a/test/integration/proxy_test.rb +++ b/test/integration/proxy_test.rb @@ -53,11 +53,11 @@ class ProxyTest < IntegrationTest private def assert_proxy_running - assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details + assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def assert_proxy_not_running - assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details + assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details end def proxy_details