Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions lib/kamal/commands/loadbalancer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ def run
"--network", "kamal",
"--detach",
"--restart", "unless-stopped",
"--publish", "80:80",
"--publish", "443:443",
"--label", label,
*run_args,
*volume_mounts))
end

Expand All @@ -35,16 +34,8 @@ def start_or_run
end

def deploy(targets: [])
target_args = targets.map { |t| "#{t}:80" }

hosts = loadbalancer_config.hosts

options = []
options << "--target=#{target_args.join(',')}"
options << "--host=#{hosts.join(',')}"
options << "--tls" if loadbalancer_config.ssl?

docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service, *options
docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service,
*loadbalancer_config.deploy_command_args(targets: targets)
end

def info
Expand Down Expand Up @@ -95,6 +86,17 @@ def container_name
end

private
# Publish/logging/docker options for the load balancer container. When the
# deploy YAML sets proxy.run, honour it (publish: false, custom ports,
# options); otherwise fall back to publishing the default 80/443.
def run_args
if (run = loadbalancer_config.run)
[ *run.publish_args, *run.logging_args, *run.options_args ]
else
[ "--publish", "80:80", "--publish", "443:443" ]
end
end

def proxy_image
[
loadbalancer_config.config.proxy_boot.image_default,
Expand Down
12 changes: 11 additions & 1 deletion lib/kamal/configuration/loadbalancer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ def initialize(config:, proxy_config:, secrets:)
def deploy_options
opts = super

# The parent strips host/tls when load balancing (the app-level proxy no
# longer owns them) — the load balancer is where they belong, so re-add.
opts[:host] = hosts if hosts.present?
opts[:tls] = proxy_config["ssl"].presence
opts[:tls] = true if ssl?

opts
end

# The load balancer fans a single service out to many targets, so unlike the
# per-app proxy deploy (which takes one target) it takes the full list and
# joins them into a single --target flag, honouring app_port for each.
def deploy_command_args(targets:)
target_arg = targets.map { |target| "#{target}:#{app_port}" }.join(",")
optionize ({ target: target_arg }).merge(deploy_options), with: "="
end

def directory
File.join config.run_directory, "loadbalancer"
end
Expand Down
2 changes: 1 addition & 1 deletion test/cli/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ class CliProxyTest < CliTestCase

run_command("loadbalancer", "deploy", fixture: :with_loadbalancer).tap do |output|
assert_match "Deploying to loadbalancer on lb.example.com with targets: 1.1.1.1, 1.1.1.2", output
assert_match "docker exec load-balancer kamal-proxy deploy app --target=1.1.1.1:80,1.1.1.2:80 --host=app.example.com --tls", output
assert_match "docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:80,1.1.1.2: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\" --host=\"app.example.com\" --tls", output
end
end

Expand Down
40 changes: 35 additions & 5 deletions test/commands/loadbalancer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,21 @@ class CommandsLoadbalancerTest < ActiveSupport::TestCase

test "run" do
assert_equal \
"echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --label org.opencontainers.image.title=kamal-loadbalancer --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
"echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --label org.opencontainers.image.title=kamal-loadbalancer --publish 80:80 --publish 443:443 --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
new_command.run.join(" ")
end

test "run honors proxy.run.publish false and options" do
@config[:proxy]["run"] = { "publish" => false, "options" => { "label" => [ "traefik.enable=true" ] } }
assert_equal \
"echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --label org.opencontainers.image.title=kamal-loadbalancer --log-opt max-size=10m --label \"traefik.enable=true\" --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
new_command.run.join(" ")
end

test "run honors custom publish ports" do
@config[:proxy]["run"] = { "http_port" => 8080, "https_port" => 8443 }
assert_equal \
"echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --label org.opencontainers.image.title=kamal-loadbalancer --publish 8080:80 --publish 8443:443 --log-opt max-size=10m --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
new_command.run.join(" ")
end

Expand All @@ -32,27 +46,43 @@ class CommandsLoadbalancerTest < ActiveSupport::TestCase

test "start_or_run" do
assert_equal \
"docker container start load-balancer || echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --label org.opencontainers.image.title=kamal-loadbalancer --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
"docker container start load-balancer || echo ghcr.io/mhenrixon/kamal-proxy:#{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION} | xargs docker run --name load-balancer --network kamal --detach --restart unless-stopped --label org.opencontainers.image.title=kamal-loadbalancer --publish 80:80 --publish 443:443 --volume kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer",
new_command.start_or_run.join(" ")
end

test "deploy with targets" do
assert_equal \
"docker exec load-balancer kamal-proxy deploy app --target=1.1.1.1:80,1.1.1.2:80 --host=app.example.com",
"docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:80,1.1.1.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" --host=\"app.example.com\"",
new_command.deploy(targets: [ "1.1.1.1", "1.1.1.2" ]).join(" ")
end

test "deploy with targets and ssl" do
@config[:proxy]["ssl"] = true
assert_equal \
"docker exec load-balancer kamal-proxy deploy app --target=1.1.1.1:80,1.1.1.2:80 --host=app.example.com --tls",
"docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:80,1.1.1.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" --host=\"app.example.com\" --tls",
new_command.deploy(targets: [ "1.1.1.1", "1.1.1.2" ]).join(" ")
end

test "deploy with multiple hosts" do
@config[:proxy]["hosts"] = [ "app1.example.com", "app2.example.com" ]
assert_equal \
"docker exec load-balancer kamal-proxy deploy app --target=1.1.1.1:80 --host=app1.example.com,app2.example.com",
"docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" --host=\"app1.example.com\" --host=\"app2.example.com\"",
new_command.deploy(targets: [ "1.1.1.1" ]).join(" ")
end

test "deploy uses app_port for targets" do
@config[:proxy]["app_port"] = 3000
assert_equal \
"docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:3000,1.1.1.2:3000\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" --host=\"app.example.com\"",
new_command.deploy(targets: [ "1.1.1.1", "1.1.1.2" ]).join(" ")
end

test "deploy propagates rich proxy options (healthcheck, response timeout, path prefix)" do
@config[:proxy]["healthcheck"] = { "interval" => 2, "timeout" => 5, "path" => "/healthz" }
@config[:proxy]["response_timeout"] = 10
@config[:proxy]["path_prefix"] = "/api"
assert_equal \
"docker exec load-balancer kamal-proxy deploy app --target=\"1.1.1.1:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --health-check-interval=\"2s\" --health-check-timeout=\"5s\" --health-check-path=\"/healthz\" --target-timeout=\"10s\" --buffer-requests --buffer-responses --path-prefix=\"/api\" --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" --host=\"app.example.com\"",
new_command.deploy(targets: [ "1.1.1.1" ]).join(" ")
end

Expand Down