Skip to content

Commit

Permalink
feat: support custom proxy image repository
Browse files Browse the repository at this point in the history
- Allow users to specify custom proxy image repository instead of using default basecamp/kamal-proxy
- Help users in regions where Docker Hub access is restricted (e.g. China) to pull proxy image from cloud vendor repositories
- Move proxy related configurations from configuration to proxy
  • Loading branch information
songjiz committed Nov 7, 2024
1 parent 9cf8da6 commit b2b71c5
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 74 deletions.
16 changes: 8 additions & 8 deletions lib/kamal/cli/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def boot

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::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.start_or_run
end
Expand All @@ -23,22 +23,22 @@ def boot

desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
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::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 :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
def boot_config(subcommand)
case subcommand
when "set"
boot_options = [
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
*(KAMAL.config.proxy_logging_args(options[:log_max_size])),
*(KAMAL.config.proxy.publish_args(options[:http_port], options[:https_port]) if options[:publish]),
*(KAMAL.config.proxy.logging_args(options[:log_max_size])),
*options[:docker_options].map { |option| "--#{option}" }
]

on(KAMAL.proxy_hosts) do |host|
execute(*KAMAL.proxy.ensure_proxy_directory)
upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file
upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy.options_file
end
when "get"
on(KAMAL.proxy_hosts) do |host|
Expand Down
4 changes: 1 addition & 3 deletions lib/kamal/commands/app/proxy.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
module Kamal::Commands::App::Proxy
delegate :proxy_container_name, to: :config

def deploy(target:)
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
end
Expand All @@ -11,6 +9,6 @@ def remove

private
def proxy_exec(*command)
docker :exec, proxy_container_name, "kamal-proxy", *command
docker :exec, config.proxy.container_name, "kamal-proxy", *command
end
end
12 changes: 6 additions & 6 deletions lib/kamal/commands/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def run
"--restart", "unless-stopped",
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
"\$\(#{get_boot_options.join(" ")}\)",
config.proxy_image
config.proxy.image
end

def start
Expand Down Expand Up @@ -65,23 +65,23 @@ def cleanup_traefik
end

def ensure_proxy_directory
make_directory config.proxy_directory
make_directory config.proxy.directory
end

def remove_proxy_directory
remove_directory config.proxy_directory
remove_directory config.proxy.directory
end

def get_boot_options
combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
combine [ :cat, config.proxy.options_file ], [ :echo, "\"#{config.proxy.options_default.join(" ")}\"" ], by: "||"
end

def reset_boot_options
remove_file config.proxy_options_file
remove_file config.proxy.options_file
end

private
def container_name
config.proxy_container_name
config.proxy.container_name
end
end
42 changes: 0 additions & 42 deletions lib/kamal/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ class Kamal::Configuration

include Validation

PROXY_MINIMUM_VERSION = "v0.8.2"
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
Expand Down Expand Up @@ -82,7 +77,6 @@ def initialize(raw_config, destination: nil, version: nil, validate: true)
ensure_unique_hosts_for_ssl_roles
end


def version=(version)
@declared_version = version
end
Expand All @@ -106,7 +100,6 @@ def minimum_version
raw_config.minimum_version
end


def roles
servers.roles
end
Expand All @@ -119,7 +112,6 @@ def accessory(name)
accessories.detect { |a| a.name == name.to_s }
end


def all_hosts
(roles + accessories).flat_map(&:hosts).uniq
end
Expand Down Expand Up @@ -180,7 +172,6 @@ def retain_containers
raw_config.retain_containers || 5
end


def volume_args
if raw_config.volumes.present?
argumentize "--volume", raw_config.volumes
Expand All @@ -193,7 +184,6 @@ def logging_args
logging.args
end


def readiness_delay
raw_config.readiness_delay || 7
end
Expand All @@ -206,7 +196,6 @@ def drain_timeout
raw_config.drain_timeout || 30
end


def run_directory
".kamal"
end
Expand All @@ -227,7 +216,6 @@ def assets_directory
File.join app_directory, "assets"
end


def hooks_path
raw_config.hooks_path || ".kamal/hooks"
end
Expand All @@ -236,7 +224,6 @@ def asset_path
raw_config.asset_path
end


def env_tags
@env_tags ||= if (tags = raw_config.env["tags"])
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
Expand All @@ -249,35 +236,6 @@ def env_tag(name)
env_tags.detect { |t| t.name == name.to_s }
end

def proxy_publish_args(http_port, https_port)
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
end

def proxy_logging_args(max_size)
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
end

def proxy_options_default
[ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ]
end

def proxy_image
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
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 to_h
{
roles: role_names,
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/configuration/docs/proxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# It is disabled by default on all other roles but can be enabled by setting
# `proxy: true` or providing a proxy configuration.
proxy:

image: "basecamp/kamal-proxy:v0.8.2"
# Hosts
#
# The hosts that will be used to serve the app. The proxy will only route requests
Expand Down
34 changes: 33 additions & 1 deletion lib/kamal/configuration/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ class Kamal::Configuration::Proxy
include Kamal::Configuration::Validation

DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ]
CONTAINER_NAME = "kamal-proxy"
DEFAULT_CONTAINER_NAME = "kamal-proxy"
MINIMUM_VERSION = "v0.8.2"
HTTP_PORT = 80
HTTPS_PORT = 443
LOG_MAX_SIZE = "10m"

delegate :argumentize, :optionize, to: Kamal::Utils

Expand All @@ -14,6 +18,14 @@ def initialize(config:, proxy_config:, context: "proxy")
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
end

def image
proxy_config.fetch("image", "basecamp/kamal-proxy:#{MINIMUM_VERSION}")
end

def container_name
DEFAULT_CONTAINER_NAME
end

def app_port
proxy_config.fetch("app_port", 80)
end
Expand Down Expand Up @@ -47,6 +59,26 @@ def deploy_options
}.compact
end

def directory
File.join config.run_directory, "proxy"
end

def options_file
File.join directory, "options"
end

def publish_args(http_port, https_port)
argumentize "--publish", [ "#{http_port}:#{HTTP_PORT}", "#{https_port}:#{HTTPS_PORT}" ]
end

def logging_args(max_size)
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
end

def options_default
[ *publish_args(HTTP_PORT, HTTPS_PORT), *logging_args(LOG_MAX_SIZE) ]
end

def deploy_command_args(target:)
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
end
Expand Down
20 changes: 10 additions & 10 deletions test/cli/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy.image}", output
end
end

Expand All @@ -18,11 +18,11 @@ class CliProxyTest < CliTestCase
exception = assert_raises do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy.image}", 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::MINIMUM_VERSION}"
ensure
Thread.report_on_exception = false
end
Expand All @@ -31,12 +31,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}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
.returns(Kamal::Configuration::Proxy::MINIMUM_VERSION)
.at_least_once

run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy.image}", output
end
ensure
Thread.report_on_exception = false
Expand All @@ -57,13 +57,13 @@ class CliProxyTest < CliTestCase
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik 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 "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy.image} 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 "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik 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 "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy.image} 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
Expand Down Expand Up @@ -182,7 +182,7 @@ class CliProxyTest < CliTestCase

SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
.returns(Kamal::Configuration::Proxy::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}}'")
Expand All @@ -198,7 +198,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 || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}", 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
Expand All @@ -221,7 +221,7 @@ class CliProxyTest < CliTestCase

SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :inspect, "kamal-proxy", "--format '{{.Config.Image}}'", "|", :cut, "-d:", "-f2")
.returns(Kamal::Configuration::PROXY_MINIMUM_VERSION)
.returns(Kamal::Configuration::Proxy::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}}'")
Expand Down
4 changes: 2 additions & 2 deletions test/commands/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class CommandsProxyTest < ActiveSupport::TestCase

test "run" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}",
new_command.run.join(" ")
end

test "run without configuration" do
@config.delete(:proxy)

assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::Proxy::MINIMUM_VERSION}",
new_command.run.join(" ")
end

Expand Down
5 changes: 5 additions & 0 deletions test/configuration/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
assert_not config.proxy.ssl?
end

test "use customized image" do
@deploy[:proxy] = { "image" => "registry.example.com/namespace/kamal-proxy" }
assert_equal "registry.example.com/namespace/kamal-proxy", config.proxy.image
end

private
def config
Kamal::Configuration.new(@deploy)
Expand Down
2 changes: 1 addition & 1 deletion test/integration/main_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b2b71c5

Please sign in to comment.