Merge pull request #1501 from basecamp/accessory-only

Allow accessory only configurations
This commit is contained in:
Donal McBreen
2025-04-18 09:58:38 +01:00
committed by GitHub
7 changed files with 99 additions and 89 deletions

View File

@@ -149,8 +149,12 @@ class Kamal::Configuration
proxy_roles.flat_map(&:name) proxy_roles.flat_map(&:name)
end end
def proxy_accessories
accessories.select(&:running_proxy?)
end
def proxy_hosts def proxy_hosts
proxy_roles.flat_map(&:hosts).uniq (proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
end end
def repository def repository
@@ -367,22 +371,26 @@ class Kamal::Configuration
end end
def ensure_required_keys_present def ensure_required_keys_present
%i[ service image registry servers ].each do |key| %i[ service image registry ].each do |key|
raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present? raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
end end
unless role(primary_role_name).present? if raw_config.servers.nil?
raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined" raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
end else
unless role(primary_role_name).present?
raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
end
if primary_role.hosts.empty? if primary_role.hosts.empty?
raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role" raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
end end
unless allow_empty_roles? unless allow_empty_roles?
roles.each do |role| roles.each do |role|
if role.hosts.empty? if role.hosts.empty?
raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true" raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
end
end end
end end
end end

View File

@@ -13,6 +13,13 @@ class Kamal::Configuration::Servers
private private
def role_names def role_names
servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort case servers_config
when Array
[ "web" ]
when NilClass
[]
else
servers_config.keys.sort
end
end end
end end

View File

@@ -1,6 +1,6 @@
class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
def validate! def validate!
validate_type! config, Array, Hash validate_type! config, Array, Hash, NilClass
validate_servers! config if config.is_a?(Array) validate_servers! config if config.is_a?(Array)
end end

View File

@@ -30,7 +30,7 @@ class ConfigurationTest < ActiveSupport::TestCase
%i[ service image registry ].each do |key| %i[ service image registry ].each do |key|
test "#{key} config required" do test "#{key} config required" do
assert_raise(Kamal::ConfigurationError) do assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1.delete key } Kamal::Configuration.new @deploy.tap { |config| config.delete key }
end end
end end
end end
@@ -38,21 +38,36 @@ class ConfigurationTest < ActiveSupport::TestCase
%w[ username password ].each do |key| %w[ username password ].each do |key|
test "registry #{key} required" do test "registry #{key} required" do
assert_raise(Kamal::ConfigurationError) do assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1[:registry].delete key } Kamal::Configuration.new @deploy.tap { |config| config[:registry].delete key }
end end
end end
end end
test "service name valid" do test "service name valid" do
assert_nothing_raised do assert_nothing_raised do
Kamal::Configuration.new(@deploy.tap { _1[:service] = "hey-app1_primary" }) Kamal::Configuration.new(@deploy.tap { |config| config[:service] = "hey-app1_primary" })
Kamal::Configuration.new(@deploy.tap { _1[:service] = "MyApp" }) Kamal::Configuration.new(@deploy.tap { |config| config[:service] = "MyApp" })
end end
end end
test "service name invalid" do test "service name invalid" do
assert_raise(Kamal::ConfigurationError) do assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { _1[:service] = "app.com" } Kamal::Configuration.new @deploy.tap { |config| config[:service] = "app.com" }
end
end
test "servers required" do
assert_raise(Kamal::ConfigurationError) do
Kamal::Configuration.new @deploy.tap { |config| config.delete(:servers) }
end
end
test "servers not required with accessories" do
assert_nothing_raised do
@deploy.delete(:servers)
@deploy[:accessories] = { "foo" => { "image" => "foo/bar", "host" => "1.1.1.1" } }
Kamal::Configuration.new(@deploy)
end end
end end
@@ -250,7 +265,7 @@ class ConfigurationTest < ActiveSupport::TestCase
test "destination required" do test "destination required" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_required_dest.yml", __dir__)) dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_required_dest.yml", __dir__))
assert_raises(Kamal::ConfigurationError) do assert_raises(ArgumentError, "You must specify a destination") do
config = Kamal::Configuration.create_from config_file: dest_config_file config = Kamal::Configuration.create_from config_file: dest_config_file
end end

View File

@@ -21,6 +21,32 @@ class AccessoryTest < IntegrationTest
assert_accessory_not_running :busybox assert_accessory_not_running :busybox
end end
test "proxied: boot, stop, start, restart, logs, remove" do
@app = "app_with_proxied_accessory"
kamal :proxy, :boot
kamal :accessory, :boot, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :stop, :netcat
assert_accessory_not_running :netcat
assert_netcat_not_found
kamal :accessory, :start, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :restart, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :remove, :netcat, "-y"
assert_accessory_not_running :netcat
assert_netcat_not_found
end
private private
def assert_accessory_running(name) def assert_accessory_running(name)
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name) assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
@@ -33,4 +59,25 @@ class AccessoryTest < IntegrationTest
def accessory_details(name) def accessory_details(name)
kamal :accessory, :details, name, capture: true kamal :accessory, :details, name, capture: true
end end
def assert_netcat_is_up
response = netcat_response
debug_response_code(response, "200")
assert_equal "200", response.code
end
def assert_netcat_not_found
response = netcat_response
debug_response_code(response, "404")
assert_equal "404", response.code
end
def netcat_response
uri = URI.parse("http://127.0.0.1:12345/up")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri)
request["Host"] = "netcat"
http.request(request)
end
end end

View File

@@ -1,7 +1,5 @@
service: app_with_proxied_accessory service: app_with_proxied_accessory
image: app_with_proxied_accessory image: app_with_proxied_accessory
servers:
- vm1
env: env:
clear: clear:
CLEAR_TOKEN: 4321 CLEAR_TOKEN: 4321
@@ -24,15 +22,13 @@ accessories:
service: custom-busybox service: custom-busybox
image: registry:4443/busybox:1.36.0 image: registry:4443/busybox:1.36.0
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done' cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
roles: host: vm1
- web
netcat: netcat:
service: netcat service: netcat
image: registry:4443/busybox:1.36.0 image: registry:4443/busybox:1.36.0
cmd: > cmd: >
sh -c 'echo "Starting netcat..."; while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello Ruby" | nc -l -p 80; done' sh -c 'echo "Starting netcat..."; while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello Ruby" | nc -l -p 80; done'
roles: host: vm1
- web
port: 12345:80 port: 12345:80
proxy: proxy:
host: netcat host: netcat

View File

@@ -1,63 +0,0 @@
require_relative "integration_test"
class ProxiedAccessoryTest < IntegrationTest
test "boot, stop, start, restart, logs, remove" do
@app = "app_with_proxied_accessory"
kamal :deploy
kamal :accessory, :boot, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :stop, :netcat
assert_accessory_not_running :netcat
assert_netcat_not_found
kamal :accessory, :start, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :restart, :netcat
assert_accessory_running :netcat
assert_netcat_is_up
kamal :accessory, :remove, :netcat, "-y"
assert_accessory_not_running :netcat
assert_netcat_not_found
end
private
def assert_accessory_running(name)
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
end
def assert_accessory_not_running(name)
assert_no_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
end
def accessory_details(name)
kamal :accessory, :details, name, capture: true
end
def assert_netcat_is_up
response = netcat_response
debug_response_code(response, "200")
assert_equal "200", response.code
end
def assert_netcat_not_found
response = netcat_response
debug_response_code(response, "404")
assert_equal "404", response.code
end
def netcat_response
uri = URI.parse("http://127.0.0.1:12345/up")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri)
request["Host"] = "netcat"
http.request(request)
end
end