From 83a5636e27ccd4da2a6856a5690e879aef6fb8d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 18:14:47 +0200 Subject: [PATCH 01/10] Pin accessories to tags --- lib/kamal/configuration/accessory.rb | 8 +++++++- lib/kamal/configuration/docs/accessory.yml | 4 +++- lib/kamal/configuration/validator/accessory.rb | 4 ++-- test/configuration/accessory_test.rb | 10 +++++++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 62979627..819f309b 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -32,7 +32,7 @@ class Kamal::Configuration::Accessory end def hosts - hosts_from_host || hosts_from_hosts || hosts_from_roles + hosts_from_host || hosts_from_hosts || hosts_from_roles || hosts_from_tags end def port @@ -206,6 +206,12 @@ class Kamal::Configuration::Accessory end end + def hosts_from_tags + if accessory_config.key?("tags") + accessory_config["tags"].flat_map { |tag| config.tag(tag)&.hosts } + end + end + def network accessory_config["network"] || DEFAULT_NETWORK end diff --git a/lib/kamal/configuration/docs/accessory.yml b/lib/kamal/configuration/docs/accessory.yml index 1852681e..7a77fd46 100644 --- a/lib/kamal/configuration/docs/accessory.yml +++ b/lib/kamal/configuration/docs/accessory.yml @@ -46,13 +46,15 @@ accessories: # Accessory hosts # - # Specify one of `host`, `hosts`, or `roles`: + # Specify one of `host`, `hosts`, `roles`, or `tags`: host: mysql-db1 hosts: - mysql-db1 - mysql-db2 roles: - mysql + tags: + - writer # Custom command # diff --git a/lib/kamal/configuration/validator/accessory.rb b/lib/kamal/configuration/validator/accessory.rb index 8b3d5b71..f870a27f 100644 --- a/lib/kamal/configuration/validator/accessory.rb +++ b/lib/kamal/configuration/validator/accessory.rb @@ -2,8 +2,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat def validate! super - if (config.keys & [ "host", "hosts", "roles" ]).size != 1 - error "specify one of `host`, `hosts` or `roles`" + if (config.keys & [ "host", "hosts", "roles", "tags" ]).size != 1 + error "specify one of `host`, `hosts`, `roles` or `tags`" end validate_docker_options!(config["options"]) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index a652c2ac..ee6d6856 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -7,7 +7,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: { - "web" => [ "1.1.1.1", "1.1.1.2" ], + "web" => [ { "1.1.1.1" => "writer" }, { "1.1.1.2" => "reader" } ], "workers" => [ "1.1.1.3", "1.1.1.4" ] }, builder: { "arch" => "amd64" }, @@ -70,6 +70,10 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "proxy" => { "host" => "monitoring.example.com" } + }, + "proxy" => { + "image" => "proxy:latest", + "tags" => [ "writer" ] } } } @@ -117,14 +121,14 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase end end - test "setting host, hosts and roles" do + test "setting host, hosts, roles and tags" do @deploy[:accessories]["mysql"]["hosts"] = [ "mysql-db1" ] @deploy[:accessories]["mysql"]["roles"] = [ "db" ] exception = assert_raises(Kamal::ConfigurationError) do Kamal::Configuration.new(@deploy) end - assert_equal "accessories/mysql: specify one of `host`, `hosts` or `roles`", exception.message + assert_equal "accessories/mysql: specify one of `host`, `hosts`, `roles` or `tags`", exception.message end test "all hosts" do From 9aac51bbd0f59f07a09ce2d701e9575b3fc069e4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:11:28 +0200 Subject: [PATCH 02/10] Extract hosts for accessories by tags --- lib/kamal/configuration/accessory.rb | 16 +++++++++++++++- test/configuration/accessory_test.rb | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 819f309b..50f70e58 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -208,7 +208,21 @@ class Kamal::Configuration::Accessory def hosts_from_tags if accessory_config.key?("tags") - accessory_config["tags"].flat_map { |tag| config.tag(tag)&.hosts } + accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) } + end + end + + def extract_hosts_from_config_with_tag(tag) + if config.raw_config.servers.is_a?(Hash) + config.raw_config.servers.flat_map do |(role, servers_in_role)| + servers_in_role.collect do |host| + if host.is_a?(Hash) && host.values.first.include?(tag) + host.keys.first + end + end + end.compact + else + [] end end diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index ee6d6856..0e75c629 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -111,6 +111,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal [ "1.1.1.5" ], @config.accessory(:mysql).hosts assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts + assert_equal [ "1.1.1.1" ], @config.accessory(:proxy).hosts end test "missing host" do From 1ca2b4d394af052e687938e4bfe3863e342af6e8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:15:44 +0200 Subject: [PATCH 03/10] Test with multiple host matches across roles --- test/configuration/accessory_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 0e75c629..fa3a8005 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -8,7 +8,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase registry: { "username" => "dhh", "password" => "secret" }, servers: { "web" => [ { "1.1.1.1" => "writer" }, { "1.1.1.2" => "reader" } ], - "workers" => [ "1.1.1.3", "1.1.1.4" ] + "workers" => [ { "1.1.1.3" => "writer" }, "1.1.1.4" ] }, builder: { "arch" => "amd64" }, env: { "REDIS_URL" => "redis://x/y" }, @@ -111,7 +111,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal [ "1.1.1.5" ], @config.accessory(:mysql).hosts assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts - assert_equal [ "1.1.1.1" ], @config.accessory(:proxy).hosts + assert_equal [ "1.1.1.1", "1.1.1.3" ], @config.accessory(:proxy).hosts end test "missing host" do From 5ad000a08e0e480c76cdbf0362aa1033e3b8cba5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:16:31 +0200 Subject: [PATCH 04/10] Unnecessary parenthesis --- lib/kamal/configuration/accessory.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 50f70e58..1317501e 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -214,7 +214,7 @@ class Kamal::Configuration::Accessory def extract_hosts_from_config_with_tag(tag) if config.raw_config.servers.is_a?(Hash) - config.raw_config.servers.flat_map do |(role, servers_in_role)| + config.raw_config.servers.flat_map do |role, servers_in_role| servers_in_role.collect do |host| if host.is_a?(Hash) && host.values.first.include?(tag) host.keys.first From 9d5a534ef86e5bf7beb8edabce5f36d374fb8e12 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:26:41 +0200 Subject: [PATCH 05/10] Refactored for clarity and style --- lib/kamal/configuration/accessory.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index 1317501e..f7edb81b 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -213,16 +213,12 @@ class Kamal::Configuration::Accessory end def extract_hosts_from_config_with_tag(tag) - if config.raw_config.servers.is_a?(Hash) - config.raw_config.servers.flat_map do |role, servers_in_role| + if (servers_with_roles = config.raw_config.servers).is_a?(Hash) + servers_with_roles.flat_map do |role, servers_in_role| servers_in_role.collect do |host| - if host.is_a?(Hash) && host.values.first.include?(tag) - host.keys.first - end - end - end.compact - else - [] + host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag) + end.compact + end end end From fb82d04aafb30f43b67309e19a9a3d835b60767b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:30:33 +0200 Subject: [PATCH 06/10] Use #filter_map instead of #collect + #compact --- lib/kamal/configuration/accessory.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index f7edb81b..a510acba 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -215,9 +215,9 @@ class Kamal::Configuration::Accessory def extract_hosts_from_config_with_tag(tag) if (servers_with_roles = config.raw_config.servers).is_a?(Hash) servers_with_roles.flat_map do |role, servers_in_role| - servers_in_role.collect do |host| + servers_in_role.filter_map do |host| host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag) - end.compact + end end end end From 299c741c1b872a96fbce90815522c8b8e5e69658 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:47:26 +0200 Subject: [PATCH 07/10] More natural api when you are just applying accessory to a single tag --- lib/kamal/configuration/accessory.rb | 4 +++- lib/kamal/configuration/docs/accessory.yml | 2 ++ lib/kamal/configuration/validator/accessory.rb | 4 ++-- test/configuration/accessory_test.rb | 10 +++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index a510acba..c15501a9 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -207,7 +207,9 @@ class Kamal::Configuration::Accessory end def hosts_from_tags - if accessory_config.key?("tags") + if accessory_config.key?("tag") + extract_hosts_from_config_with_tag(accessory_config["tag"]) + elsif accessory_config.key?("tags") accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) } end end diff --git a/lib/kamal/configuration/docs/accessory.yml b/lib/kamal/configuration/docs/accessory.yml index 7a77fd46..1033edaa 100644 --- a/lib/kamal/configuration/docs/accessory.yml +++ b/lib/kamal/configuration/docs/accessory.yml @@ -53,8 +53,10 @@ accessories: - mysql-db2 roles: - mysql + tag: writer tags: - writer + - reader # Custom command # diff --git a/lib/kamal/configuration/validator/accessory.rb b/lib/kamal/configuration/validator/accessory.rb index f870a27f..96c5aafc 100644 --- a/lib/kamal/configuration/validator/accessory.rb +++ b/lib/kamal/configuration/validator/accessory.rb @@ -2,8 +2,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat def validate! super - if (config.keys & [ "host", "hosts", "roles", "tags" ]).size != 1 - error "specify one of `host`, `hosts`, `roles` or `tags`" + if (config.keys & [ "host", "hosts", "roles", "tag", "tags" ]).size != 1 + error "specify one of `host`, `hosts`, `roles`, `tag` or `tags`" end validate_docker_options!(config["options"]) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index fa3a8005..e8af6556 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -73,7 +73,11 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase }, "proxy" => { "image" => "proxy:latest", - "tags" => [ "writer" ] + "tags" => [ "writer", "reader" ] + }, + "logger" => { + "image" => "logger:latest", + "tag" => "writer" } } } @@ -111,7 +115,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal [ "1.1.1.5" ], @config.accessory(:mysql).hosts assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts - assert_equal [ "1.1.1.1", "1.1.1.3" ], @config.accessory(:proxy).hosts + assert_equal [ "1.1.1.1", "1.1.1.3", "1.1.1.2" ], @config.accessory(:proxy).hosts end test "missing host" do @@ -129,7 +133,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase exception = assert_raises(Kamal::ConfigurationError) do Kamal::Configuration.new(@deploy) end - assert_equal "accessories/mysql: specify one of `host`, `hosts`, `roles` or `tags`", exception.message + assert_equal "accessories/mysql: specify one of `host`, `hosts`, `roles`, `tag` or `tags`", exception.message end test "all hosts" do From 7bfb2ed9f2777db7b87293121ddc0359350a795e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 9 May 2025 21:50:07 +0200 Subject: [PATCH 08/10] Actually test the fixture for singular --- test/configuration/accessory_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index e8af6556..179a5f5d 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -116,6 +116,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase assert_equal [ "1.1.1.6", "1.1.1.7" ], @config.accessory(:redis).hosts assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.accessory(:monitoring).hosts assert_equal [ "1.1.1.1", "1.1.1.3", "1.1.1.2" ], @config.accessory(:proxy).hosts + assert_equal [ "1.1.1.1", "1.1.1.3" ], @config.accessory(:logger).hosts end test "missing host" do From 78c9d610cf61e2dbe45d1899a7f47a3b6e879a96 Mon Sep 17 00:00:00 2001 From: Krzysztof Duda Date: Mon, 12 May 2025 10:55:15 +0200 Subject: [PATCH 09/10] Add a singular role --- lib/kamal/configuration/accessory.rb | 6 +++++- lib/kamal/configuration/docs/accessory.yml | 3 ++- lib/kamal/configuration/validator/accessory.rb | 4 ++-- test/configuration/accessory_test.rb | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/kamal/configuration/accessory.rb b/lib/kamal/configuration/accessory.rb index c15501a9..c1dbad57 100644 --- a/lib/kamal/configuration/accessory.rb +++ b/lib/kamal/configuration/accessory.rb @@ -201,7 +201,9 @@ class Kamal::Configuration::Accessory end def hosts_from_roles - if accessory_config.key?("roles") + if accessory_config.key?("role") + config.role(accessory_config["role"])&.hosts + elsif accessory_config.key?("roles") accessory_config["roles"].flat_map { |role| config.role(role)&.hosts } end end @@ -231,6 +233,8 @@ class Kamal::Configuration::Accessory def ensure_valid_roles if accessory_config["roles"] && (missing_roles = accessory_config["roles"] - config.roles.map(&:name)).any? raise Kamal::ConfigurationError, "accessories/#{name}: unknown roles #{missing_roles.join(", ")}" + elsif accessory_config["role"] && !config.role(accessory_config["role"]) + raise Kamal::ConfigurationError, "accessories/#{name}: unknown role #{accessory_config["role"]}" end end end diff --git a/lib/kamal/configuration/docs/accessory.yml b/lib/kamal/configuration/docs/accessory.yml index 1033edaa..3983211a 100644 --- a/lib/kamal/configuration/docs/accessory.yml +++ b/lib/kamal/configuration/docs/accessory.yml @@ -46,11 +46,12 @@ accessories: # Accessory hosts # - # Specify one of `host`, `hosts`, `roles`, or `tags`: + # Specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`: host: mysql-db1 hosts: - mysql-db1 - mysql-db2 + role: mysql roles: - mysql tag: writer diff --git a/lib/kamal/configuration/validator/accessory.rb b/lib/kamal/configuration/validator/accessory.rb index 96c5aafc..564cfc7e 100644 --- a/lib/kamal/configuration/validator/accessory.rb +++ b/lib/kamal/configuration/validator/accessory.rb @@ -2,8 +2,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat def validate! super - if (config.keys & [ "host", "hosts", "roles", "tag", "tags" ]).size != 1 - error "specify one of `host`, `hosts`, `roles`, `tag` or `tags`" + if (config.keys & [ "host", "hosts", "role", "roles", "tag", "tags" ]).size != 1 + error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`" end validate_docker_options!(config["options"]) diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index 179a5f5d..0f13b33e 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -55,7 +55,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "service" => "custom-monitoring", "image" => "monitoring:latest", "registry" => { "server" => "other.registry", "username" => "user", "password" => "pw" }, - "roles" => [ "web" ], + "role" => "web", "port" => "4321:4321", "labels" => { "cache" => "true" @@ -134,7 +134,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase exception = assert_raises(Kamal::ConfigurationError) do Kamal::Configuration.new(@deploy) end - assert_equal "accessories/mysql: specify one of `host`, `hosts`, `roles`, `tag` or `tags`", exception.message + assert_equal "accessories/mysql: specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`", exception.message end test "all hosts" do From 031f55ecf745bd7a3aec173d360bd96dc64b643c Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 13 May 2025 09:50:52 +0100 Subject: [PATCH 10/10] Bump version for 2.6.0 --- Gemfile.lock | 2 +- lib/kamal/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f6328178..bcc38f0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - kamal (2.5.3) + kamal (2.6.0) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) diff --git a/lib/kamal/version.rb b/lib/kamal/version.rb index 3099a7cc..9525c872 100644 --- a/lib/kamal/version.rb +++ b/lib/kamal/version.rb @@ -1,3 +1,3 @@ module Kamal - VERSION = "2.5.3" + VERSION = "2.6.0" end